MikelWL commited on
Commit
d7e3980
·
1 Parent(s): 2568a5f

Add rubric instructions and attributes to frameworks

Browse files
README.md CHANGED
@@ -103,7 +103,7 @@ After the conversation completes, the app runs post-conversation analysis and po
103
 
104
  - An **analysis framework** (stored in SQLite at `DB_PATH`) defines:
105
  - bottom-up instructions + attributes
106
- - rubric attributes
107
  - top-down instructions + attributes + codebook categories (optional per-category descriptions)
108
  - The **active framework is selected per run** using the dropdown next to the **📊 Analysis** header (AI-to-AI / Human-to-AI / Upload Text).
109
  - The **Configuration** panel is used to create/duplicate/delete frameworks and edit them; selection there is for editing/review only.
 
103
 
104
  - An **analysis framework** (stored in SQLite at `DB_PATH`) defines:
105
  - bottom-up instructions + attributes
106
+ - rubric instructions + attributes
107
  - top-down instructions + attributes + codebook categories (optional per-category descriptions)
108
  - The **active framework is selected per run** using the dropdown next to the **📊 Analysis** header (AI-to-AI / Human-to-AI / Upload Text).
109
  - The **Configuration** panel is used to create/duplicate/delete frameworks and edit them; selection there is for editing/review only.
backend/api/analysis_routes.py CHANGED
@@ -169,6 +169,7 @@ async def _analyze_from_text(
169
  analysis_system_prompt=effective_analysis_system_prompt if isinstance(effective_analysis_system_prompt, str) else None,
170
  bottom_up_instructions=template_record.bottom_up_instructions if template_record else None,
171
  bottom_up_attributes=template_record.bottom_up_attributes if template_record else None,
 
172
  rubric_attributes=template_record.rubric_attributes if template_record else None,
173
  top_down_instructions=template_record.top_down_instructions if template_record else None,
174
  top_down_attributes=template_record.top_down_attributes if template_record else None,
@@ -198,6 +199,7 @@ async def _analyze_from_text(
198
  "analysis_system_prompt": effective_analysis_system_prompt if isinstance(effective_analysis_system_prompt, str) else None,
199
  "bottom_up_instructions": template_record.bottom_up_instructions if template_record else None,
200
  "bottom_up_attributes": template_record.bottom_up_attributes if template_record else None,
 
201
  "rubric_attributes": template_record.rubric_attributes if template_record else None,
202
  "top_down_instructions": template_record.top_down_instructions if template_record else None,
203
  "top_down_attributes": template_record.top_down_attributes if template_record else None,
 
169
  analysis_system_prompt=effective_analysis_system_prompt if isinstance(effective_analysis_system_prompt, str) else None,
170
  bottom_up_instructions=template_record.bottom_up_instructions if template_record else None,
171
  bottom_up_attributes=template_record.bottom_up_attributes if template_record else None,
172
+ rubric_instructions=template_record.rubric_instructions if template_record else None,
173
  rubric_attributes=template_record.rubric_attributes if template_record else None,
174
  top_down_instructions=template_record.top_down_instructions if template_record else None,
175
  top_down_attributes=template_record.top_down_attributes if template_record else None,
 
199
  "analysis_system_prompt": effective_analysis_system_prompt if isinstance(effective_analysis_system_prompt, str) else None,
200
  "bottom_up_instructions": template_record.bottom_up_instructions if template_record else None,
201
  "bottom_up_attributes": template_record.bottom_up_attributes if template_record else None,
202
+ "rubric_instructions": template_record.rubric_instructions if template_record else None,
203
  "rubric_attributes": template_record.rubric_attributes if template_record else None,
204
  "top_down_instructions": template_record.top_down_instructions if template_record else None,
205
  "top_down_attributes": template_record.top_down_attributes if template_record else None,
backend/api/analysis_template_routes.py CHANGED
@@ -13,6 +13,7 @@ from .storage_service import get_persona_store
13
  from backend.core.analysis_knobs import (
14
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
15
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
 
16
  DEFAULT_RUBRIC_ATTRIBUTES,
17
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
18
  DEFAULT_TOP_DOWN_ATTRIBUTES,
@@ -71,6 +72,7 @@ class AnalysisTemplateDetailResponse(BaseModel):
71
  version_id: str
72
  bottom_up_instructions: str = ""
73
  bottom_up_attributes: List[str] = Field(default_factory=list)
 
74
  rubric_attributes: List[str] = Field(default_factory=list)
75
  top_down_instructions: str = ""
76
  top_down_attributes: List[str] = Field(default_factory=list)
@@ -85,6 +87,7 @@ class CreateAnalysisTemplateRequest(BaseModel):
85
  class UpdateAnalysisTemplateRequest(BaseModel):
86
  bottom_up_instructions: Optional[str] = None
87
  bottom_up_attributes: Optional[List[str]] = None
 
88
  rubric_attributes: Optional[List[str]] = None
89
  top_down_instructions: Optional[str] = None
90
  top_down_attributes: Optional[List[str]] = None
@@ -120,6 +123,7 @@ async def get_template(template_id: str) -> AnalysisTemplateDetailResponse:
120
  name=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("name") or "Default top-down codebook (v1)"),
121
  bottom_up_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_instructions") or DEFAULT_BOTTOM_UP_INSTRUCTIONS),
122
  bottom_up_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_attributes") or DEFAULT_BOTTOM_UP_ATTRIBUTES), # type: ignore[list-item]
 
123
  rubric_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_attributes") or DEFAULT_RUBRIC_ATTRIBUTES), # type: ignore[list-item]
124
  top_down_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_instructions") or DEFAULT_TOP_DOWN_INSTRUCTIONS),
125
  top_down_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_attributes") or DEFAULT_TOP_DOWN_ATTRIBUTES), # type: ignore[list-item]
@@ -137,6 +141,7 @@ async def get_template(template_id: str) -> AnalysisTemplateDetailResponse:
137
  version_id=record.current_version_id,
138
  bottom_up_instructions=record.bottom_up_instructions,
139
  bottom_up_attributes=record.bottom_up_attributes,
 
140
  rubric_attributes=record.rubric_attributes,
141
  top_down_instructions=record.top_down_instructions,
142
  top_down_attributes=record.top_down_attributes,
@@ -154,6 +159,7 @@ async def create_template(payload: CreateAnalysisTemplateRequest) -> AnalysisTem
154
  categories: List[Dict[str, str]] = []
155
  bottom_up_instructions = DEFAULT_BOTTOM_UP_INSTRUCTIONS
156
  bottom_up_attributes = list(DEFAULT_BOTTOM_UP_ATTRIBUTES)
 
157
  rubric_attributes = list(DEFAULT_RUBRIC_ATTRIBUTES)
158
  top_down_instructions = DEFAULT_TOP_DOWN_INSTRUCTIONS
159
  top_down_attributes = list(DEFAULT_TOP_DOWN_ATTRIBUTES)
@@ -164,6 +170,7 @@ async def create_template(payload: CreateAnalysisTemplateRequest) -> AnalysisTem
164
  categories = src.categories
165
  bottom_up_instructions = src.bottom_up_instructions
166
  bottom_up_attributes = list(src.bottom_up_attributes)
 
167
  rubric_attributes = list(src.rubric_attributes)
168
  top_down_instructions = src.top_down_instructions
169
  top_down_attributes = list(src.top_down_attributes)
@@ -174,6 +181,7 @@ async def create_template(payload: CreateAnalysisTemplateRequest) -> AnalysisTem
174
  name=name,
175
  bottom_up_instructions=bottom_up_instructions,
176
  bottom_up_attributes=bottom_up_attributes,
 
177
  rubric_attributes=rubric_attributes,
178
  top_down_instructions=top_down_instructions,
179
  top_down_attributes=top_down_attributes,
@@ -190,6 +198,7 @@ async def create_template(payload: CreateAnalysisTemplateRequest) -> AnalysisTem
190
  version_id=record.current_version_id,
191
  bottom_up_instructions=record.bottom_up_instructions,
192
  bottom_up_attributes=record.bottom_up_attributes,
 
193
  rubric_attributes=record.rubric_attributes,
194
  top_down_instructions=record.top_down_instructions,
195
  top_down_attributes=record.top_down_attributes,
@@ -212,19 +221,21 @@ async def update_template(template_id: str, payload: UpdateAnalysisTemplateReque
212
 
213
  bottom_up_instructions = payload.bottom_up_instructions if isinstance(payload.bottom_up_instructions, str) else record.bottom_up_instructions
214
  bottom_up_attributes = payload.bottom_up_attributes if isinstance(payload.bottom_up_attributes, list) else record.bottom_up_attributes
 
215
  rubric_attributes = payload.rubric_attributes if isinstance(payload.rubric_attributes, list) else record.rubric_attributes
216
  top_down_instructions = payload.top_down_instructions if isinstance(payload.top_down_instructions, str) else record.top_down_instructions
217
  top_down_attributes = payload.top_down_attributes if isinstance(payload.top_down_attributes, list) else record.top_down_attributes
218
 
219
  try:
220
  await store.update_analysis_template(
221
- template_id=template_id,
222
- bottom_up_instructions=bottom_up_instructions,
223
- bottom_up_attributes=bottom_up_attributes,
224
- rubric_attributes=rubric_attributes,
225
- top_down_instructions=top_down_instructions,
226
- top_down_attributes=top_down_attributes,
227
- categories=categories,
 
228
  )
229
  except PermissionError as e:
230
  raise HTTPException(status_code=403, detail=str(e))
@@ -241,6 +252,7 @@ async def update_template(template_id: str, payload: UpdateAnalysisTemplateReque
241
  version_id=updated.current_version_id,
242
  bottom_up_instructions=updated.bottom_up_instructions,
243
  bottom_up_attributes=updated.bottom_up_attributes,
 
244
  rubric_attributes=updated.rubric_attributes,
245
  top_down_instructions=updated.top_down_instructions,
246
  top_down_attributes=updated.top_down_attributes,
 
13
  from backend.core.analysis_knobs import (
14
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
15
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
16
+ DEFAULT_RUBRIC_INSTRUCTIONS,
17
  DEFAULT_RUBRIC_ATTRIBUTES,
18
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
19
  DEFAULT_TOP_DOWN_ATTRIBUTES,
 
72
  version_id: str
73
  bottom_up_instructions: str = ""
74
  bottom_up_attributes: List[str] = Field(default_factory=list)
75
+ rubric_instructions: str = ""
76
  rubric_attributes: List[str] = Field(default_factory=list)
77
  top_down_instructions: str = ""
78
  top_down_attributes: List[str] = Field(default_factory=list)
 
87
  class UpdateAnalysisTemplateRequest(BaseModel):
88
  bottom_up_instructions: Optional[str] = None
89
  bottom_up_attributes: Optional[List[str]] = None
90
+ rubric_instructions: Optional[str] = None
91
  rubric_attributes: Optional[List[str]] = None
92
  top_down_instructions: Optional[str] = None
93
  top_down_attributes: Optional[List[str]] = None
 
123
  name=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("name") or "Default top-down codebook (v1)"),
124
  bottom_up_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_instructions") or DEFAULT_BOTTOM_UP_INSTRUCTIONS),
125
  bottom_up_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_attributes") or DEFAULT_BOTTOM_UP_ATTRIBUTES), # type: ignore[list-item]
126
+ rubric_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_instructions") or DEFAULT_RUBRIC_INSTRUCTIONS),
127
  rubric_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_attributes") or DEFAULT_RUBRIC_ATTRIBUTES), # type: ignore[list-item]
128
  top_down_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_instructions") or DEFAULT_TOP_DOWN_INSTRUCTIONS),
129
  top_down_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_attributes") or DEFAULT_TOP_DOWN_ATTRIBUTES), # type: ignore[list-item]
 
141
  version_id=record.current_version_id,
142
  bottom_up_instructions=record.bottom_up_instructions,
143
  bottom_up_attributes=record.bottom_up_attributes,
144
+ rubric_instructions=record.rubric_instructions,
145
  rubric_attributes=record.rubric_attributes,
146
  top_down_instructions=record.top_down_instructions,
147
  top_down_attributes=record.top_down_attributes,
 
159
  categories: List[Dict[str, str]] = []
160
  bottom_up_instructions = DEFAULT_BOTTOM_UP_INSTRUCTIONS
161
  bottom_up_attributes = list(DEFAULT_BOTTOM_UP_ATTRIBUTES)
162
+ rubric_instructions = DEFAULT_RUBRIC_INSTRUCTIONS
163
  rubric_attributes = list(DEFAULT_RUBRIC_ATTRIBUTES)
164
  top_down_instructions = DEFAULT_TOP_DOWN_INSTRUCTIONS
165
  top_down_attributes = list(DEFAULT_TOP_DOWN_ATTRIBUTES)
 
170
  categories = src.categories
171
  bottom_up_instructions = src.bottom_up_instructions
172
  bottom_up_attributes = list(src.bottom_up_attributes)
173
+ rubric_instructions = src.rubric_instructions
174
  rubric_attributes = list(src.rubric_attributes)
175
  top_down_instructions = src.top_down_instructions
176
  top_down_attributes = list(src.top_down_attributes)
 
181
  name=name,
182
  bottom_up_instructions=bottom_up_instructions,
183
  bottom_up_attributes=bottom_up_attributes,
184
+ rubric_instructions=rubric_instructions,
185
  rubric_attributes=rubric_attributes,
186
  top_down_instructions=top_down_instructions,
187
  top_down_attributes=top_down_attributes,
 
198
  version_id=record.current_version_id,
199
  bottom_up_instructions=record.bottom_up_instructions,
200
  bottom_up_attributes=record.bottom_up_attributes,
201
+ rubric_instructions=record.rubric_instructions,
202
  rubric_attributes=record.rubric_attributes,
203
  top_down_instructions=record.top_down_instructions,
204
  top_down_attributes=record.top_down_attributes,
 
221
 
222
  bottom_up_instructions = payload.bottom_up_instructions if isinstance(payload.bottom_up_instructions, str) else record.bottom_up_instructions
223
  bottom_up_attributes = payload.bottom_up_attributes if isinstance(payload.bottom_up_attributes, list) else record.bottom_up_attributes
224
+ rubric_instructions = payload.rubric_instructions if isinstance(payload.rubric_instructions, str) else record.rubric_instructions
225
  rubric_attributes = payload.rubric_attributes if isinstance(payload.rubric_attributes, list) else record.rubric_attributes
226
  top_down_instructions = payload.top_down_instructions if isinstance(payload.top_down_instructions, str) else record.top_down_instructions
227
  top_down_attributes = payload.top_down_attributes if isinstance(payload.top_down_attributes, list) else record.top_down_attributes
228
 
229
  try:
230
  await store.update_analysis_template(
231
+ template_id=template_id,
232
+ bottom_up_instructions=bottom_up_instructions,
233
+ bottom_up_attributes=bottom_up_attributes,
234
+ rubric_instructions=rubric_instructions,
235
+ rubric_attributes=rubric_attributes,
236
+ top_down_instructions=top_down_instructions,
237
+ top_down_attributes=top_down_attributes,
238
+ categories=categories,
239
  )
240
  except PermissionError as e:
241
  raise HTTPException(status_code=403, detail=str(e))
 
252
  version_id=updated.current_version_id,
253
  bottom_up_instructions=updated.bottom_up_instructions,
254
  bottom_up_attributes=updated.bottom_up_attributes,
255
+ rubric_instructions=updated.rubric_instructions,
256
  rubric_attributes=updated.rubric_attributes,
257
  top_down_instructions=updated.top_down_instructions,
258
  top_down_attributes=updated.top_down_attributes,
backend/api/conversation_service.py CHANGED
@@ -50,6 +50,7 @@ from backend.core.analysis_knobs import (
50
  compile_analysis_rules_block,
51
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
52
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
 
53
  DEFAULT_RUBRIC_ATTRIBUTES,
54
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
55
  DEFAULT_TOP_DOWN_ATTRIBUTES,
@@ -106,6 +107,7 @@ async def run_resource_agent_analysis(
106
  analysis_system_prompt: Optional[str] = None,
107
  bottom_up_instructions: Optional[str] = None,
108
  bottom_up_attributes: Optional[List[str]] = None,
 
109
  rubric_attributes: Optional[List[str]] = None,
110
  top_down_instructions: Optional[str] = None,
111
  top_down_attributes: Optional[List[str]] = None,
@@ -175,8 +177,11 @@ async def run_resource_agent_analysis(
175
  + "\n\n"
176
  + compile_analysis_rules_block(bottom_up_attributes, defaults=DEFAULT_BOTTOM_UP_ATTRIBUTES)
177
  ).strip()
 
178
  rubric_system_prompt = (
179
  common_system_prompt
 
 
180
  + "\n\n"
181
  + compile_analysis_rules_block(rubric_attributes, defaults=DEFAULT_RUBRIC_ATTRIBUTES)
182
  ).strip()
@@ -435,6 +440,7 @@ class ConversationInfo:
435
  analysis_system_prompt: str = ""
436
  bottom_up_instructions: str = ""
437
  bottom_up_attributes: List[str] = field(default_factory=list)
 
438
  rubric_attributes: List[str] = field(default_factory=list)
439
  top_down_instructions: str = ""
440
  top_down_attributes: List[str] = field(default_factory=list)
@@ -464,6 +470,7 @@ class HumanChatInfo:
464
  analysis_system_prompt: str = ""
465
  bottom_up_instructions: str = ""
466
  bottom_up_attributes: List[str] = field(default_factory=list)
 
467
  rubric_attributes: List[str] = field(default_factory=list)
468
  top_down_instructions: str = ""
469
  top_down_attributes: List[str] = field(default_factory=list)
@@ -537,6 +544,7 @@ class ConversationService:
537
  "template_version_id": record.current_version_id,
538
  "bottom_up_instructions": record.bottom_up_instructions,
539
  "bottom_up_attributes": list(record.bottom_up_attributes),
 
540
  "rubric_attributes": list(record.rubric_attributes),
541
  "top_down_instructions": record.top_down_instructions,
542
  "top_down_attributes": list(record.top_down_attributes),
@@ -552,6 +560,7 @@ class ConversationService:
552
  "template_version_id": record.current_version_id,
553
  "bottom_up_instructions": record.bottom_up_instructions,
554
  "bottom_up_attributes": list(record.bottom_up_attributes),
 
555
  "rubric_attributes": list(record.rubric_attributes),
556
  "top_down_instructions": record.top_down_instructions,
557
  "top_down_attributes": list(record.top_down_attributes),
@@ -562,6 +571,7 @@ class ConversationService:
562
  "template_version_id": "",
563
  "bottom_up_instructions": DEFAULT_BOTTOM_UP_INSTRUCTIONS,
564
  "bottom_up_attributes": list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
 
565
  "rubric_attributes": list(DEFAULT_RUBRIC_ATTRIBUTES),
566
  "top_down_instructions": DEFAULT_TOP_DOWN_INSTRUCTIONS,
567
  "top_down_attributes": list(DEFAULT_TOP_DOWN_ATTRIBUTES),
@@ -612,6 +622,7 @@ class ConversationService:
612
  template = await self._resolve_top_down_template(top_down_codebook_template_id)
613
  resolved_bottom_instructions = str(template.get("bottom_up_instructions") or "").strip() or DEFAULT_BOTTOM_UP_INSTRUCTIONS
614
  resolved_top_down_instructions = str(template.get("top_down_instructions") or "").strip() or DEFAULT_TOP_DOWN_INSTRUCTIONS
 
615
  bua = template.get("bottom_up_attributes")
616
  ra = template.get("rubric_attributes")
617
  tda = template.get("top_down_attributes")
@@ -637,6 +648,7 @@ class ConversationService:
637
  analysis_system_prompt=resolved_analysis_prompt,
638
  bottom_up_instructions=resolved_bottom_instructions,
639
  bottom_up_attributes=resolved_bottom_attrs,
 
640
  rubric_attributes=resolved_rubric_attrs,
641
  top_down_instructions=resolved_top_down_instructions,
642
  top_down_attributes=resolved_top_down_attrs,
@@ -995,6 +1007,7 @@ class ConversationService:
995
  template = await self._resolve_top_down_template(top_down_codebook_template_id)
996
  resolved_bottom_instructions = str(template.get("bottom_up_instructions") or "").strip() or DEFAULT_BOTTOM_UP_INSTRUCTIONS
997
  resolved_top_down_instructions = str(template.get("top_down_instructions") or "").strip() or DEFAULT_TOP_DOWN_INSTRUCTIONS
 
998
  bua = template.get("bottom_up_attributes")
999
  ra = template.get("rubric_attributes")
1000
  tda = template.get("top_down_attributes")
@@ -1021,6 +1034,7 @@ class ConversationService:
1021
  analysis_system_prompt=resolved_analysis_prompt,
1022
  bottom_up_instructions=resolved_bottom_instructions,
1023
  bottom_up_attributes=resolved_bottom_attrs,
 
1024
  rubric_attributes=resolved_rubric_attrs,
1025
  top_down_instructions=resolved_top_down_instructions,
1026
  top_down_attributes=resolved_top_down_attrs,
@@ -1312,6 +1326,7 @@ class ConversationService:
1312
  analysis_system_prompt=getattr(conv_info, "analysis_system_prompt", None),
1313
  bottom_up_instructions=getattr(conv_info, "bottom_up_instructions", None),
1314
  bottom_up_attributes=getattr(conv_info, "bottom_up_attributes", None),
 
1315
  rubric_attributes=getattr(conv_info, "rubric_attributes", None),
1316
  top_down_instructions=getattr(conv_info, "top_down_instructions", None),
1317
  top_down_attributes=getattr(conv_info, "top_down_attributes", None),
@@ -1367,11 +1382,12 @@ class ConversationService:
1367
  "asked_question_ids": asked_question_ids,
1368
  },
1369
  "analysis": {
1370
- "analysis_system_prompt": getattr(conv_info, "analysis_system_prompt", None),
1371
- "bottom_up_instructions": getattr(conv_info, "bottom_up_instructions", None),
1372
- "bottom_up_attributes": getattr(conv_info, "bottom_up_attributes", None),
1373
- "rubric_attributes": getattr(conv_info, "rubric_attributes", None),
1374
- "top_down_instructions": getattr(conv_info, "top_down_instructions", None),
 
1375
  "top_down_attributes": getattr(conv_info, "top_down_attributes", None),
1376
  "top_down_codebook_template_id": getattr(conv_info, "top_down_template_id", None),
1377
  "top_down_codebook_template_version_id": getattr(conv_info, "top_down_template_version_id", None),
 
50
  compile_analysis_rules_block,
51
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
52
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
53
+ DEFAULT_RUBRIC_INSTRUCTIONS,
54
  DEFAULT_RUBRIC_ATTRIBUTES,
55
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
56
  DEFAULT_TOP_DOWN_ATTRIBUTES,
 
107
  analysis_system_prompt: Optional[str] = None,
108
  bottom_up_instructions: Optional[str] = None,
109
  bottom_up_attributes: Optional[List[str]] = None,
110
+ rubric_instructions: Optional[str] = None,
111
  rubric_attributes: Optional[List[str]] = None,
112
  top_down_instructions: Optional[str] = None,
113
  top_down_attributes: Optional[List[str]] = None,
 
177
  + "\n\n"
178
  + compile_analysis_rules_block(bottom_up_attributes, defaults=DEFAULT_BOTTOM_UP_ATTRIBUTES)
179
  ).strip()
180
+ resolved_rubric_instructions = (rubric_instructions or "").strip() or DEFAULT_RUBRIC_INSTRUCTIONS
181
  rubric_system_prompt = (
182
  common_system_prompt
183
+ + "\n\nCare experience rubric instructions:\n"
184
+ + resolved_rubric_instructions.strip()
185
  + "\n\n"
186
  + compile_analysis_rules_block(rubric_attributes, defaults=DEFAULT_RUBRIC_ATTRIBUTES)
187
  ).strip()
 
440
  analysis_system_prompt: str = ""
441
  bottom_up_instructions: str = ""
442
  bottom_up_attributes: List[str] = field(default_factory=list)
443
+ rubric_instructions: str = ""
444
  rubric_attributes: List[str] = field(default_factory=list)
445
  top_down_instructions: str = ""
446
  top_down_attributes: List[str] = field(default_factory=list)
 
470
  analysis_system_prompt: str = ""
471
  bottom_up_instructions: str = ""
472
  bottom_up_attributes: List[str] = field(default_factory=list)
473
+ rubric_instructions: str = ""
474
  rubric_attributes: List[str] = field(default_factory=list)
475
  top_down_instructions: str = ""
476
  top_down_attributes: List[str] = field(default_factory=list)
 
544
  "template_version_id": record.current_version_id,
545
  "bottom_up_instructions": record.bottom_up_instructions,
546
  "bottom_up_attributes": list(record.bottom_up_attributes),
547
+ "rubric_instructions": record.rubric_instructions,
548
  "rubric_attributes": list(record.rubric_attributes),
549
  "top_down_instructions": record.top_down_instructions,
550
  "top_down_attributes": list(record.top_down_attributes),
 
560
  "template_version_id": record.current_version_id,
561
  "bottom_up_instructions": record.bottom_up_instructions,
562
  "bottom_up_attributes": list(record.bottom_up_attributes),
563
+ "rubric_instructions": record.rubric_instructions,
564
  "rubric_attributes": list(record.rubric_attributes),
565
  "top_down_instructions": record.top_down_instructions,
566
  "top_down_attributes": list(record.top_down_attributes),
 
571
  "template_version_id": "",
572
  "bottom_up_instructions": DEFAULT_BOTTOM_UP_INSTRUCTIONS,
573
  "bottom_up_attributes": list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
574
+ "rubric_instructions": DEFAULT_RUBRIC_INSTRUCTIONS,
575
  "rubric_attributes": list(DEFAULT_RUBRIC_ATTRIBUTES),
576
  "top_down_instructions": DEFAULT_TOP_DOWN_INSTRUCTIONS,
577
  "top_down_attributes": list(DEFAULT_TOP_DOWN_ATTRIBUTES),
 
622
  template = await self._resolve_top_down_template(top_down_codebook_template_id)
623
  resolved_bottom_instructions = str(template.get("bottom_up_instructions") or "").strip() or DEFAULT_BOTTOM_UP_INSTRUCTIONS
624
  resolved_top_down_instructions = str(template.get("top_down_instructions") or "").strip() or DEFAULT_TOP_DOWN_INSTRUCTIONS
625
+ resolved_rubric_instructions = str(template.get("rubric_instructions") or "").strip() or DEFAULT_RUBRIC_INSTRUCTIONS
626
  bua = template.get("bottom_up_attributes")
627
  ra = template.get("rubric_attributes")
628
  tda = template.get("top_down_attributes")
 
648
  analysis_system_prompt=resolved_analysis_prompt,
649
  bottom_up_instructions=resolved_bottom_instructions,
650
  bottom_up_attributes=resolved_bottom_attrs,
651
+ rubric_instructions=resolved_rubric_instructions,
652
  rubric_attributes=resolved_rubric_attrs,
653
  top_down_instructions=resolved_top_down_instructions,
654
  top_down_attributes=resolved_top_down_attrs,
 
1007
  template = await self._resolve_top_down_template(top_down_codebook_template_id)
1008
  resolved_bottom_instructions = str(template.get("bottom_up_instructions") or "").strip() or DEFAULT_BOTTOM_UP_INSTRUCTIONS
1009
  resolved_top_down_instructions = str(template.get("top_down_instructions") or "").strip() or DEFAULT_TOP_DOWN_INSTRUCTIONS
1010
+ resolved_rubric_instructions = str(template.get("rubric_instructions") or "").strip() or DEFAULT_RUBRIC_INSTRUCTIONS
1011
  bua = template.get("bottom_up_attributes")
1012
  ra = template.get("rubric_attributes")
1013
  tda = template.get("top_down_attributes")
 
1034
  analysis_system_prompt=resolved_analysis_prompt,
1035
  bottom_up_instructions=resolved_bottom_instructions,
1036
  bottom_up_attributes=resolved_bottom_attrs,
1037
+ rubric_instructions=resolved_rubric_instructions,
1038
  rubric_attributes=resolved_rubric_attrs,
1039
  top_down_instructions=resolved_top_down_instructions,
1040
  top_down_attributes=resolved_top_down_attrs,
 
1326
  analysis_system_prompt=getattr(conv_info, "analysis_system_prompt", None),
1327
  bottom_up_instructions=getattr(conv_info, "bottom_up_instructions", None),
1328
  bottom_up_attributes=getattr(conv_info, "bottom_up_attributes", None),
1329
+ rubric_instructions=getattr(conv_info, "rubric_instructions", None),
1330
  rubric_attributes=getattr(conv_info, "rubric_attributes", None),
1331
  top_down_instructions=getattr(conv_info, "top_down_instructions", None),
1332
  top_down_attributes=getattr(conv_info, "top_down_attributes", None),
 
1382
  "asked_question_ids": asked_question_ids,
1383
  },
1384
  "analysis": {
1385
+ "analysis_system_prompt": getattr(conv_info, "analysis_system_prompt", None),
1386
+ "bottom_up_instructions": getattr(conv_info, "bottom_up_instructions", None),
1387
+ "bottom_up_attributes": getattr(conv_info, "bottom_up_attributes", None),
1388
+ "rubric_instructions": getattr(conv_info, "rubric_instructions", None),
1389
+ "rubric_attributes": getattr(conv_info, "rubric_attributes", None),
1390
+ "top_down_instructions": getattr(conv_info, "top_down_instructions", None),
1391
  "top_down_attributes": getattr(conv_info, "top_down_attributes", None),
1392
  "top_down_codebook_template_id": getattr(conv_info, "top_down_template_id", None),
1393
  "top_down_codebook_template_version_id": getattr(conv_info, "top_down_template_version_id", None),
backend/core/analysis_knobs.py CHANGED
@@ -11,6 +11,10 @@ DEFAULT_TOP_DOWN_INSTRUCTIONS: str = (
11
  "You are performing top-down coding using the provided codebook categories. For each category, extract the most relevant codes/items supported by evidence."
12
  )
13
 
 
 
 
 
14
 
15
  DEFAULT_BOTTOM_UP_ATTRIBUTES: List[str] = [
16
  "Prefer fewer, higher-confidence items.",
 
11
  "You are performing top-down coding using the provided codebook categories. For each category, extract the most relevant codes/items supported by evidence."
12
  )
13
 
14
+ DEFAULT_RUBRIC_INSTRUCTIONS: str = (
15
+ "You are performing a care experience rubric analysis. Summarize the overall experience and list the key reasons for each bucket."
16
+ )
17
+
18
 
19
  DEFAULT_BOTTOM_UP_ATTRIBUTES: List[str] = [
20
  "Prefer fewer, higher-confidence items.",
backend/core/default_analysis_templates.py CHANGED
@@ -5,6 +5,7 @@ from typing import Dict, List
5
  from backend.core.analysis_knobs import (
6
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
7
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
 
8
  DEFAULT_RUBRIC_ATTRIBUTES,
9
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
10
  DEFAULT_TOP_DOWN_ATTRIBUTES,
@@ -15,6 +16,7 @@ DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE: Dict[str, object] = {
15
  "name": "Default top-down codebook (v1)",
16
  "bottom_up_instructions": DEFAULT_BOTTOM_UP_INSTRUCTIONS,
17
  "bottom_up_attributes": list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
 
18
  "rubric_attributes": list(DEFAULT_RUBRIC_ATTRIBUTES),
19
  "top_down_instructions": DEFAULT_TOP_DOWN_INSTRUCTIONS,
20
  "top_down_attributes": list(DEFAULT_TOP_DOWN_ATTRIBUTES),
 
5
  from backend.core.analysis_knobs import (
6
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
7
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
8
+ DEFAULT_RUBRIC_INSTRUCTIONS,
9
  DEFAULT_RUBRIC_ATTRIBUTES,
10
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
11
  DEFAULT_TOP_DOWN_ATTRIBUTES,
 
16
  "name": "Default top-down codebook (v1)",
17
  "bottom_up_instructions": DEFAULT_BOTTOM_UP_INSTRUCTIONS,
18
  "bottom_up_attributes": list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
19
+ "rubric_instructions": DEFAULT_RUBRIC_INSTRUCTIONS,
20
  "rubric_attributes": list(DEFAULT_RUBRIC_ATTRIBUTES),
21
  "top_down_instructions": DEFAULT_TOP_DOWN_INSTRUCTIONS,
22
  "top_down_attributes": list(DEFAULT_TOP_DOWN_ATTRIBUTES),
backend/core/persona_seed.py CHANGED
@@ -7,6 +7,7 @@ from backend.core.universal_prompts import DEFAULT_PATIENT_SYSTEM_PROMPT, DEFAUL
7
  from backend.core.analysis_knobs import (
8
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
9
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
 
10
  DEFAULT_RUBRIC_ATTRIBUTES,
11
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
12
  DEFAULT_TOP_DOWN_ATTRIBUTES,
@@ -38,6 +39,7 @@ async def seed_defaults_overwrite(*, store: SQLitePersonaStore) -> None:
38
  name=template_name,
39
  bottom_up_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_instructions") or DEFAULT_BOTTOM_UP_INSTRUCTIONS),
40
  bottom_up_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_attributes") or DEFAULT_BOTTOM_UP_ATTRIBUTES), # type: ignore[list-item]
 
41
  rubric_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_attributes") or DEFAULT_RUBRIC_ATTRIBUTES), # type: ignore[list-item]
42
  top_down_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_instructions") or DEFAULT_TOP_DOWN_INSTRUCTIONS),
43
  top_down_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_attributes") or DEFAULT_TOP_DOWN_ATTRIBUTES), # type: ignore[list-item]
@@ -62,6 +64,7 @@ async def seed_defaults_overwrite(*, store: SQLitePersonaStore) -> None:
62
  not isinstance(t.bottom_up_instructions, str)
63
  or not t.bottom_up_instructions.strip()
64
  or not (isinstance(t.bottom_up_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.bottom_up_attributes))
 
65
  or not (isinstance(t.rubric_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.rubric_attributes))
66
  or not (isinstance(t.top_down_instructions, str) and t.top_down_instructions.strip())
67
  or not (isinstance(t.top_down_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.top_down_attributes))
@@ -72,6 +75,7 @@ async def seed_defaults_overwrite(*, store: SQLitePersonaStore) -> None:
72
  template_id=t.template_id,
73
  bottom_up_instructions=t.bottom_up_instructions.strip() if isinstance(t.bottom_up_instructions, str) and t.bottom_up_instructions.strip() else DEFAULT_BOTTOM_UP_INSTRUCTIONS,
74
  bottom_up_attributes=t.bottom_up_attributes if t.bottom_up_attributes else list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
 
75
  rubric_attributes=t.rubric_attributes if t.rubric_attributes else list(DEFAULT_RUBRIC_ATTRIBUTES),
76
  top_down_instructions=t.top_down_instructions.strip() if isinstance(t.top_down_instructions, str) and t.top_down_instructions.strip() else DEFAULT_TOP_DOWN_INSTRUCTIONS,
77
  top_down_attributes=t.top_down_attributes if t.top_down_attributes else list(DEFAULT_TOP_DOWN_ATTRIBUTES),
 
7
  from backend.core.analysis_knobs import (
8
  DEFAULT_BOTTOM_UP_INSTRUCTIONS,
9
  DEFAULT_BOTTOM_UP_ATTRIBUTES,
10
+ DEFAULT_RUBRIC_INSTRUCTIONS,
11
  DEFAULT_RUBRIC_ATTRIBUTES,
12
  DEFAULT_TOP_DOWN_INSTRUCTIONS,
13
  DEFAULT_TOP_DOWN_ATTRIBUTES,
 
39
  name=template_name,
40
  bottom_up_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_instructions") or DEFAULT_BOTTOM_UP_INSTRUCTIONS),
41
  bottom_up_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("bottom_up_attributes") or DEFAULT_BOTTOM_UP_ATTRIBUTES), # type: ignore[list-item]
42
+ rubric_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_instructions") or DEFAULT_RUBRIC_INSTRUCTIONS),
43
  rubric_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("rubric_attributes") or DEFAULT_RUBRIC_ATTRIBUTES), # type: ignore[list-item]
44
  top_down_instructions=str(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_instructions") or DEFAULT_TOP_DOWN_INSTRUCTIONS),
45
  top_down_attributes=list(DEFAULT_TOP_DOWN_CODEBOOK_TEMPLATE.get("top_down_attributes") or DEFAULT_TOP_DOWN_ATTRIBUTES), # type: ignore[list-item]
 
64
  not isinstance(t.bottom_up_instructions, str)
65
  or not t.bottom_up_instructions.strip()
66
  or not (isinstance(t.bottom_up_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.bottom_up_attributes))
67
+ or not isinstance(t.rubric_instructions, str)
68
  or not (isinstance(t.rubric_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.rubric_attributes))
69
  or not (isinstance(t.top_down_instructions, str) and t.top_down_instructions.strip())
70
  or not (isinstance(t.top_down_attributes, list) and any(isinstance(x, str) and x.strip() for x in t.top_down_attributes))
 
75
  template_id=t.template_id,
76
  bottom_up_instructions=t.bottom_up_instructions.strip() if isinstance(t.bottom_up_instructions, str) and t.bottom_up_instructions.strip() else DEFAULT_BOTTOM_UP_INSTRUCTIONS,
77
  bottom_up_attributes=t.bottom_up_attributes if t.bottom_up_attributes else list(DEFAULT_BOTTOM_UP_ATTRIBUTES),
78
+ rubric_instructions=t.rubric_instructions.strip() if isinstance(t.rubric_instructions, str) and t.rubric_instructions.strip() else DEFAULT_RUBRIC_INSTRUCTIONS,
79
  rubric_attributes=t.rubric_attributes if t.rubric_attributes else list(DEFAULT_RUBRIC_ATTRIBUTES),
80
  top_down_instructions=t.top_down_instructions.strip() if isinstance(t.top_down_instructions, str) and t.top_down_instructions.strip() else DEFAULT_TOP_DOWN_INSTRUCTIONS,
81
  top_down_attributes=t.top_down_attributes if t.top_down_attributes else list(DEFAULT_TOP_DOWN_ATTRIBUTES),
backend/storage/models.py CHANGED
@@ -73,6 +73,7 @@ class AnalysisTemplateRecord:
73
  updated_at: str
74
  bottom_up_instructions: str
75
  bottom_up_attributes: List[str]
 
76
  rubric_attributes: List[str]
77
  top_down_instructions: str
78
  top_down_attributes: List[str]
 
73
  updated_at: str
74
  bottom_up_instructions: str
75
  bottom_up_attributes: List[str]
76
+ rubric_instructions: str
77
  rubric_attributes: List[str]
78
  top_down_instructions: str
79
  top_down_attributes: List[str]
backend/storage/sqlite_persona_store.py CHANGED
@@ -212,6 +212,7 @@ class SQLitePersonaStore:
212
  updated_at=row[6],
213
  bottom_up_instructions=_clean_str(content.get("bottom_up_instructions")),
214
  bottom_up_attributes=_clean_str_list(content.get("bottom_up_attributes")),
 
215
  rubric_attributes=_clean_str_list(content.get("rubric_attributes")),
216
  top_down_instructions=_clean_str(content.get("top_down_instructions")),
217
  top_down_attributes=_clean_str_list(content.get("top_down_attributes")),
@@ -270,6 +271,7 @@ class SQLitePersonaStore:
270
  updated_at=row[6],
271
  bottom_up_instructions=_clean_str(content.get("bottom_up_instructions")),
272
  bottom_up_attributes=_clean_str_list(content.get("bottom_up_attributes")),
 
273
  rubric_attributes=_clean_str_list(content.get("rubric_attributes")),
274
  top_down_instructions=_clean_str(content.get("top_down_instructions")),
275
  top_down_attributes=_clean_str_list(content.get("top_down_attributes")),
@@ -283,6 +285,7 @@ class SQLitePersonaStore:
283
  name: str,
284
  bottom_up_instructions: str,
285
  bottom_up_attributes: List[str],
 
286
  rubric_attributes: List[str],
287
  top_down_instructions: str,
288
  top_down_attributes: List[str],
@@ -297,6 +300,7 @@ class SQLitePersonaStore:
297
  raise ValueError("categories are required")
298
  bui = _clean_str(bottom_up_instructions)
299
  bua = _clean_str_list(bottom_up_attributes)
 
300
  ra = _clean_str_list(rubric_attributes)
301
  tdi = _clean_str(top_down_instructions)
302
  tda = _clean_str_list(top_down_attributes)
@@ -307,6 +311,7 @@ class SQLitePersonaStore:
307
  {
308
  "bottom_up_instructions": bui,
309
  "bottom_up_attributes": bua,
 
310
  "rubric_attributes": ra,
311
  "top_down_instructions": tdi,
312
  "top_down_attributes": tda,
@@ -357,6 +362,7 @@ class SQLitePersonaStore:
357
  name: str,
358
  bottom_up_instructions: str,
359
  bottom_up_attributes: List[str],
 
360
  rubric_attributes: List[str],
361
  top_down_instructions: str,
362
  top_down_attributes: List[str],
@@ -370,6 +376,7 @@ class SQLitePersonaStore:
370
  raise ValueError("categories are required")
371
  bui = _clean_str(bottom_up_instructions)
372
  bua = _clean_str_list(bottom_up_attributes)
 
373
  ra = _clean_str_list(rubric_attributes)
374
  tdi = _clean_str(top_down_instructions)
375
  tda = _clean_str_list(top_down_attributes)
@@ -381,6 +388,7 @@ class SQLitePersonaStore:
381
  {
382
  "bottom_up_instructions": bui,
383
  "bottom_up_attributes": bua,
 
384
  "rubric_attributes": ra,
385
  "top_down_instructions": tdi,
386
  "top_down_attributes": tda,
@@ -419,6 +427,7 @@ class SQLitePersonaStore:
419
  template_id: str,
420
  bottom_up_instructions: str,
421
  bottom_up_attributes: List[str],
 
422
  rubric_attributes: List[str],
423
  top_down_instructions: str,
424
  top_down_attributes: List[str],
@@ -432,6 +441,7 @@ class SQLitePersonaStore:
432
  raise ValueError("categories are required")
433
  bui = _clean_str(bottom_up_instructions)
434
  bua = _clean_str_list(bottom_up_attributes)
 
435
  ra = _clean_str_list(rubric_attributes)
436
  tdi = _clean_str(top_down_instructions)
437
  tda = _clean_str_list(top_down_attributes)
@@ -442,6 +452,7 @@ class SQLitePersonaStore:
442
  {
443
  "bottom_up_instructions": bui,
444
  "bottom_up_attributes": bua,
 
445
  "rubric_attributes": ra,
446
  "top_down_instructions": tdi,
447
  "top_down_attributes": tda,
 
212
  updated_at=row[6],
213
  bottom_up_instructions=_clean_str(content.get("bottom_up_instructions")),
214
  bottom_up_attributes=_clean_str_list(content.get("bottom_up_attributes")),
215
+ rubric_instructions=_clean_str(content.get("rubric_instructions")),
216
  rubric_attributes=_clean_str_list(content.get("rubric_attributes")),
217
  top_down_instructions=_clean_str(content.get("top_down_instructions")),
218
  top_down_attributes=_clean_str_list(content.get("top_down_attributes")),
 
271
  updated_at=row[6],
272
  bottom_up_instructions=_clean_str(content.get("bottom_up_instructions")),
273
  bottom_up_attributes=_clean_str_list(content.get("bottom_up_attributes")),
274
+ rubric_instructions=_clean_str(content.get("rubric_instructions")),
275
  rubric_attributes=_clean_str_list(content.get("rubric_attributes")),
276
  top_down_instructions=_clean_str(content.get("top_down_instructions")),
277
  top_down_attributes=_clean_str_list(content.get("top_down_attributes")),
 
285
  name: str,
286
  bottom_up_instructions: str,
287
  bottom_up_attributes: List[str],
288
+ rubric_instructions: str,
289
  rubric_attributes: List[str],
290
  top_down_instructions: str,
291
  top_down_attributes: List[str],
 
300
  raise ValueError("categories are required")
301
  bui = _clean_str(bottom_up_instructions)
302
  bua = _clean_str_list(bottom_up_attributes)
303
+ ri = _clean_str(rubric_instructions)
304
  ra = _clean_str_list(rubric_attributes)
305
  tdi = _clean_str(top_down_instructions)
306
  tda = _clean_str_list(top_down_attributes)
 
311
  {
312
  "bottom_up_instructions": bui,
313
  "bottom_up_attributes": bua,
314
+ "rubric_instructions": ri,
315
  "rubric_attributes": ra,
316
  "top_down_instructions": tdi,
317
  "top_down_attributes": tda,
 
362
  name: str,
363
  bottom_up_instructions: str,
364
  bottom_up_attributes: List[str],
365
+ rubric_instructions: str,
366
  rubric_attributes: List[str],
367
  top_down_instructions: str,
368
  top_down_attributes: List[str],
 
376
  raise ValueError("categories are required")
377
  bui = _clean_str(bottom_up_instructions)
378
  bua = _clean_str_list(bottom_up_attributes)
379
+ ri = _clean_str(rubric_instructions)
380
  ra = _clean_str_list(rubric_attributes)
381
  tdi = _clean_str(top_down_instructions)
382
  tda = _clean_str_list(top_down_attributes)
 
388
  {
389
  "bottom_up_instructions": bui,
390
  "bottom_up_attributes": bua,
391
+ "rubric_instructions": ri,
392
  "rubric_attributes": ra,
393
  "top_down_instructions": tdi,
394
  "top_down_attributes": tda,
 
427
  template_id: str,
428
  bottom_up_instructions: str,
429
  bottom_up_attributes: List[str],
430
+ rubric_instructions: str,
431
  rubric_attributes: List[str],
432
  top_down_instructions: str,
433
  top_down_attributes: List[str],
 
441
  raise ValueError("categories are required")
442
  bui = _clean_str(bottom_up_instructions)
443
  bua = _clean_str_list(bottom_up_attributes)
444
+ ri = _clean_str(rubric_instructions)
445
  ra = _clean_str_list(rubric_attributes)
446
  tdi = _clean_str(top_down_instructions)
447
  tda = _clean_str_list(top_down_attributes)
 
452
  {
453
  "bottom_up_instructions": bui,
454
  "bottom_up_attributes": bua,
455
+ "rubric_instructions": ri,
456
  "rubric_attributes": ra,
457
  "top_down_instructions": tdi,
458
  "top_down_attributes": tda,
docs/capabilities.md CHANGED
@@ -104,7 +104,7 @@ In the UI, each column populates as its pass completes, while later passes show
104
 
105
  - An **analysis framework** defines:
106
  - bottom-up instructions + attributes
107
- - rubric attributes
108
  - top-down instructions + attributes + codebook categories (optional per-category descriptions)
109
  - Frameworks are managed in **Configuration** (create/duplicate/delete + edit the framework).
110
  - The **active framework is selected per run** using the dropdown next to the **📊 Analysis** header in:
 
104
 
105
  - An **analysis framework** defines:
106
  - bottom-up instructions + attributes
107
+ - rubric instructions + attributes
108
  - top-down instructions + attributes + codebook categories (optional per-category descriptions)
109
  - Frameworks are managed in **Configuration** (create/duplicate/delete + edit the framework).
110
  - The **active framework is selected per run** using the dropdown next to the **📊 Analysis** header in:
frontend/pages/config_view.py CHANGED
@@ -13,6 +13,7 @@ def get_config_view_js() -> str:
13
  };
14
  const DEFAULT_BOTTOM_UP_INSTRUCTIONS = `You are performing bottom-up thematic analysis. Identify the most prominent themes in the conversation, with no a priori concepts; the themes are emergent from the text.`;
15
  const DEFAULT_TOP_DOWN_INSTRUCTIONS = `You are performing top-down coding using the provided codebook categories. For each category, extract the most relevant codes/items supported by evidence.`;
 
16
 
17
  const BOTTOM_UP_DEFAULT_ATTRIBUTES = [
18
  "Prefer fewer, higher-confidence items.",
@@ -92,6 +93,7 @@ Requirements:
92
  const [patientPersonaDetail, setPatientPersonaDetail] = React.useState(null);
93
  const [bottomUpInstructions, setBottomUpInstructions] = React.useState(() => '');
94
  const [bottomUpAttributeItems, setBottomUpAttributeItems] = React.useState(() => []);
 
95
  const [rubricAttributeItems, setRubricAttributeItems] = React.useState(() => []);
96
  const [topDownInstructions, setTopDownInstructions] = React.useState(() => '');
97
  const [topDownAttributeItems, setTopDownAttributeItems] = React.useState(() => []);
@@ -231,22 +233,27 @@ Requirements:
231
  })
232
  .catch(() => null);
233
  };
234
- const addBottomUpAttribute = () => setBottomUpAttributeItems((prev) => ([...(prev || []), '' ]));
235
- const updateBottomUpAttribute = (idx, text) => setBottomUpAttributeItems((prev) => (prev || []).map((v, i) => (i === idx ? text : v)));
236
- const removeBottomUpAttribute = (idx) => setBottomUpAttributeItems((prev) => (prev || []).filter((_, i) => i !== idx));
237
 
238
- const addTopDownAttribute = () => setTopDownAttributeItems((prev) => ([...(prev || []), '' ]));
239
- const updateTopDownAttribute = (idx, text) => setTopDownAttributeItems((prev) => (prev || []).map((v, i) => (i === idx ? text : v)));
240
- const removeTopDownAttribute = (idx) => setTopDownAttributeItems((prev) => (prev || []).filter((_, i) => i !== idx));
 
 
 
 
241
 
242
  const applyBottomUpDefaults = () => {
243
  setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
244
  setBottomUpAttributeItems([...(BOTTOM_UP_DEFAULT_ATTRIBUTES || [])]);
245
  };
246
 
247
- const applyRubricDefaults = () => {
248
- setRubricAttributeItems([...(RUBRIC_DEFAULT_ATTRIBUTES || [])]);
249
- };
 
250
 
251
  const applyTopDownDefaults = () => {
252
  setTopDownInstructions(DEFAULT_TOP_DOWN_INSTRUCTIONS);
@@ -309,13 +316,18 @@ Requirements:
309
  } else {
310
  setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
311
  }
312
- const bua = Array.isArray(t?.bottom_up_attributes) ? t.bottom_up_attributes : [];
313
- const buaClean = bua.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
314
- setBottomUpAttributeItems(buaClean.length ? buaClean : [ ...(BOTTOM_UP_DEFAULT_ATTRIBUTES || []) ]);
315
 
316
- const ra = Array.isArray(t?.rubric_attributes) ? t.rubric_attributes : [];
317
- const raClean = ra.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
318
- setRubricAttributeItems(raClean.length ? raClean : [ ...(RUBRIC_DEFAULT_ATTRIBUTES || []) ]);
 
 
 
 
 
319
 
320
  const tda = Array.isArray(t?.top_down_attributes) ? t.top_down_attributes : [];
321
  const tdaClean = tda.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
@@ -334,15 +346,16 @@ Requirements:
334
  description: (c && typeof c.description === 'string') ? c.description : '',
335
  })));
336
  })
337
- .catch(() => {
338
- setSelectedTemplateDetail(null);
339
- setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
340
- setBottomUpAttributeItems([ ...(BOTTOM_UP_DEFAULT_ATTRIBUTES || []) ]);
341
- setRubricAttributeItems([ ...(RUBRIC_DEFAULT_ATTRIBUTES || []) ]);
342
- setTopDownInstructions(DEFAULT_TOP_DOWN_INSTRUCTIONS);
343
- setTopDownAttributeItems([ ...(TOP_DOWN_DEFAULT_ATTRIBUTES || []) ]);
344
- setSelectedTemplateCategories([]);
345
- });
 
346
  }, [selectedTemplateId]);
347
 
348
  React.useEffect(() => {
@@ -428,12 +441,13 @@ Requirements:
428
  method: 'PUT',
429
  headers: { 'Content-Type': 'application/json' },
430
  body: JSON.stringify({
431
- bottom_up_instructions: (bottomUpInstructions || '').trim(),
432
- bottom_up_attributes: (bottomUpAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
433
- rubric_attributes: (rubricAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
434
- top_down_instructions: (topDownInstructions || '').trim(),
435
- top_down_attributes: (topDownAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
436
- categories: (selectedTemplateCategories || []).map((c) => ({
 
437
  category_id: (c && typeof c.category_id === 'string') ? c.category_id : '',
438
  label: (c && typeof c.label === 'string') ? c.label.trim() : '',
439
  description: (c && typeof c.description === 'string') ? c.description.trim() : '',
@@ -1001,8 +1015,64 @@ Requirements:
1001
 
1002
  <div>
1003
  <div className="text-sm font-semibold text-slate-700 mb-2">Care experience rubric</div>
1004
- <div className="bg-slate-50 border border-slate-200 rounded-lg p-4 text-sm text-slate-600">
1005
- Placeholder: rubric-specific settings will go here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  </div>
1007
  </div>
1008
 
 
13
  };
14
  const DEFAULT_BOTTOM_UP_INSTRUCTIONS = `You are performing bottom-up thematic analysis. Identify the most prominent themes in the conversation, with no a priori concepts; the themes are emergent from the text.`;
15
  const DEFAULT_TOP_DOWN_INSTRUCTIONS = `You are performing top-down coding using the provided codebook categories. For each category, extract the most relevant codes/items supported by evidence.`;
16
+ const DEFAULT_RUBRIC_INSTRUCTIONS = `You are performing a care experience rubric analysis. Summarize the overall experience and list the key reasons for each bucket.`;
17
 
18
  const BOTTOM_UP_DEFAULT_ATTRIBUTES = [
19
  "Prefer fewer, higher-confidence items.",
 
93
  const [patientPersonaDetail, setPatientPersonaDetail] = React.useState(null);
94
  const [bottomUpInstructions, setBottomUpInstructions] = React.useState(() => '');
95
  const [bottomUpAttributeItems, setBottomUpAttributeItems] = React.useState(() => []);
96
+ const [rubricInstructions, setRubricInstructions] = React.useState(() => '');
97
  const [rubricAttributeItems, setRubricAttributeItems] = React.useState(() => []);
98
  const [topDownInstructions, setTopDownInstructions] = React.useState(() => '');
99
  const [topDownAttributeItems, setTopDownAttributeItems] = React.useState(() => []);
 
233
  })
234
  .catch(() => null);
235
  };
236
+ const addBottomUpAttribute = () => setBottomUpAttributeItems((prev) => ([...(prev || []), '' ]));
237
+ const updateBottomUpAttribute = (idx, text) => setBottomUpAttributeItems((prev) => (prev || []).map((v, i) => (i === idx ? text : v)));
238
+ const removeBottomUpAttribute = (idx) => setBottomUpAttributeItems((prev) => (prev || []).filter((_, i) => i !== idx));
239
 
240
+ const addRubricAttribute = () => setRubricAttributeItems((prev) => ([...(prev || []), '' ]));
241
+ const updateRubricAttribute = (idx, text) => setRubricAttributeItems((prev) => (prev || []).map((v, i) => (i === idx ? text : v)));
242
+ const removeRubricAttribute = (idx) => setRubricAttributeItems((prev) => (prev || []).filter((_, i) => i !== idx));
243
+
244
+ const addTopDownAttribute = () => setTopDownAttributeItems((prev) => ([...(prev || []), '' ]));
245
+ const updateTopDownAttribute = (idx, text) => setTopDownAttributeItems((prev) => (prev || []).map((v, i) => (i === idx ? text : v)));
246
+ const removeTopDownAttribute = (idx) => setTopDownAttributeItems((prev) => (prev || []).filter((_, i) => i !== idx));
247
 
248
  const applyBottomUpDefaults = () => {
249
  setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
250
  setBottomUpAttributeItems([...(BOTTOM_UP_DEFAULT_ATTRIBUTES || [])]);
251
  };
252
 
253
+ const applyRubricDefaults = () => {
254
+ setRubricInstructions(DEFAULT_RUBRIC_INSTRUCTIONS);
255
+ setRubricAttributeItems([...(RUBRIC_DEFAULT_ATTRIBUTES || [])]);
256
+ };
257
 
258
  const applyTopDownDefaults = () => {
259
  setTopDownInstructions(DEFAULT_TOP_DOWN_INSTRUCTIONS);
 
316
  } else {
317
  setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
318
  }
319
+ const bua = Array.isArray(t?.bottom_up_attributes) ? t.bottom_up_attributes : [];
320
+ const buaClean = bua.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
321
+ setBottomUpAttributeItems(buaClean.length ? buaClean : [ ...(BOTTOM_UP_DEFAULT_ATTRIBUTES || []) ]);
322
 
323
+ if (typeof t?.rubric_instructions === 'string' && t.rubric_instructions.trim()) {
324
+ setRubricInstructions(t.rubric_instructions);
325
+ } else {
326
+ setRubricInstructions(DEFAULT_RUBRIC_INSTRUCTIONS);
327
+ }
328
+ const ra = Array.isArray(t?.rubric_attributes) ? t.rubric_attributes : [];
329
+ const raClean = ra.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
330
+ setRubricAttributeItems(raClean.length ? raClean : [ ...(RUBRIC_DEFAULT_ATTRIBUTES || []) ]);
331
 
332
  const tda = Array.isArray(t?.top_down_attributes) ? t.top_down_attributes : [];
333
  const tdaClean = tda.map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0);
 
346
  description: (c && typeof c.description === 'string') ? c.description : '',
347
  })));
348
  })
349
+ .catch(() => {
350
+ setSelectedTemplateDetail(null);
351
+ setBottomUpInstructions(DEFAULT_BOTTOM_UP_INSTRUCTIONS);
352
+ setBottomUpAttributeItems([ ...(BOTTOM_UP_DEFAULT_ATTRIBUTES || []) ]);
353
+ setRubricInstructions(DEFAULT_RUBRIC_INSTRUCTIONS);
354
+ setRubricAttributeItems([ ...(RUBRIC_DEFAULT_ATTRIBUTES || []) ]);
355
+ setTopDownInstructions(DEFAULT_TOP_DOWN_INSTRUCTIONS);
356
+ setTopDownAttributeItems([ ...(TOP_DOWN_DEFAULT_ATTRIBUTES || []) ]);
357
+ setSelectedTemplateCategories([]);
358
+ });
359
  }, [selectedTemplateId]);
360
 
361
  React.useEffect(() => {
 
441
  method: 'PUT',
442
  headers: { 'Content-Type': 'application/json' },
443
  body: JSON.stringify({
444
+ bottom_up_instructions: (bottomUpInstructions || '').trim(),
445
+ bottom_up_attributes: (bottomUpAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
446
+ rubric_instructions: (rubricInstructions || '').trim(),
447
+ rubric_attributes: (rubricAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
448
+ top_down_instructions: (topDownInstructions || '').trim(),
449
+ top_down_attributes: (topDownAttributeItems || []).map((x) => (typeof x === 'string' ? x.trim() : '')).filter((x) => x.length > 0),
450
+ categories: (selectedTemplateCategories || []).map((c) => ({
451
  category_id: (c && typeof c.category_id === 'string') ? c.category_id : '',
452
  label: (c && typeof c.label === 'string') ? c.label.trim() : '',
453
  description: (c && typeof c.description === 'string') ? c.description.trim() : '',
 
1015
 
1016
  <div>
1017
  <div className="text-sm font-semibold text-slate-700 mb-2">Care experience rubric</div>
1018
+ <div className="flex items-center justify-between gap-3 mb-2">
1019
+ <label className="block text-sm font-semibold text-slate-700">Care experience rubric instructions</label>
1020
+ <button
1021
+ type="button"
1022
+ onClick={applyRubricDefaults}
1023
+ disabled={!(selectedTemplateDetail?.is_default === false)}
1024
+ className="bg-slate-100 hover:bg-slate-200 disabled:hover:bg-slate-100 disabled:opacity-50 text-slate-800 px-3 py-2 rounded-lg text-sm font-semibold transition-all border border-slate-300"
1025
+ >
1026
+ Apply defaults
1027
+ </button>
1028
+ </div>
1029
+ <textarea
1030
+ disabled={!(selectedTemplateDetail?.is_default === false)}
1031
+ className={`w-full border border-slate-300 rounded-lg px-3 py-2 text-sm h-32 ${selectedTemplateDetail?.is_default === false ? 'bg-white' : 'bg-slate-50 text-slate-700'}`}
1032
+ value={rubricInstructions || ''}
1033
+ onChange={(e) => setRubricInstructions(e.target.value)}
1034
+ placeholder="Rubric instructions..."
1035
+ />
1036
+
1037
+ <div className="flex items-center justify-between gap-3 mt-4 mb-2">
1038
+ <label className="block text-sm font-semibold text-slate-700">Rubric attributes</label>
1039
+ </div>
1040
+ <div className="space-y-2">
1041
+ {(rubricAttributeItems || []).length === 0 ? (
1042
+ <div className="text-sm text-slate-500">No attributes yet.</div>
1043
+ ) : (
1044
+ (rubricAttributeItems || []).map((attr, idx) => (
1045
+ <div key={idx} className="flex items-start gap-2">
1046
+ <div className="mt-2 text-xs font-mono text-slate-500 w-10 shrink-0">{String(idx + 1).padStart(2, '0')}</div>
1047
+ <input
1048
+ disabled={!(selectedTemplateDetail?.is_default === false)}
1049
+ className={`flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm ${selectedTemplateDetail?.is_default === false ? 'bg-white' : 'bg-slate-50 text-slate-600'}`}
1050
+ value={attr || ''}
1051
+ onChange={(e) => updateRubricAttribute(idx, e.target.value)}
1052
+ placeholder="Type an attribute..."
1053
+ />
1054
+ {selectedTemplateDetail?.is_default === false && (
1055
+ <button
1056
+ type="button"
1057
+ onClick={() => removeRubricAttribute(idx)}
1058
+ className="text-slate-600 hover:text-red-600 px-2 py-2 text-sm"
1059
+ title="Remove attribute"
1060
+ >
1061
+
1062
+ </button>
1063
+ )}
1064
+ </div>
1065
+ ))
1066
+ )}
1067
+ {selectedTemplateDetail?.is_default === false && (
1068
+ <button
1069
+ type="button"
1070
+ onClick={addRubricAttribute}
1071
+ className="bg-slate-100 hover:bg-slate-200 text-slate-800 px-3 py-2 rounded-lg text-sm font-semibold transition-all border border-slate-300"
1072
+ >
1073
+ + Add attribute
1074
+ </button>
1075
+ )}
1076
  </div>
1077
  </div>
1078