feat: extract individual values from stage outputs (Issue #47)
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

FEATURE: Basis-Analysen Einzelwerte
Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile)
Jetzt:  bmi → 26.6 (eigene Zeile)
        weight → 85.2kg (eigene Zeile)

BACKEND: JSON-Extraktion
- Stage outputs (JSON) → extract individual fields
- extracted_values dict sammelt alle Einzelwerte
- Deduplizierung: Gleiche Keys nur einmal
- Flags:
  - is_extracted: true → Wert aus Stage-Output extrahiert
  - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus

BEISPIEL Stage 1 Output:
{
  "stage_1_body": {
    "bmi": 26.6,
    "weight": "85.2 kg",
    "trend": "sinkend"
  }
}

→ Metadata:
{
  "bmi": {
    value: "26.6",
    description: "Aus Stage 1 (stage_1_body)",
    is_extracted: true
  },
  "weight": {
    value: "85.2 kg",
    description: "Aus Stage 1 (stage_1_body)",
    is_extracted: true
  },
  "stage_1_body": {
    value: "{\"bmi\": 26.6, ...}",
    description: "Rohdaten Stage 1 (Basis-Analyse JSON)",
    is_stage_raw: true
  }
}

FRONTEND: Smart Filtering
Normal-Modus:
- Zeigt: Einzelwerte (bmi, weight, trend)
- Versteckt: Rohdaten (stage_1_body JSON)
- Filter: is_stage_raw === false

Experten-Modus:
- Zeigt: Alles (Einzelwerte + Rohdaten)
- Rohdaten: Grauer Hintergrund + 🔬 Icon

VISUAL Indicators:
↳ bmi        → Extrahierter Wert (grün)
  weight     → Normaler Platzhalter (accent)
🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten)

ERGEBNIS:
┌──────────────────────────────────────────┐
│ 📊 Verwendete Werte (8) (+2 ausgeblendet)│
│ ┌────────────────────────────────────────┐│
│ │ weight_aktuell │ 85.2 kg   │ Gewicht ││ ← Normal
│ │ ↳ bmi          │ 26.6      │ Aus St..││ ← Extrahiert
│ │ ↳ trend        │ sinkend   │ Aus St..││ ← Extrahiert
│ └────────────────────────────────────────┘│
└──────────────────────────────────────────┘

Experten-Modus zusätzlich:
│ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON

version: 9.9.0 (feature)
module: prompts 2.4.0, insights 1.7.0
This commit is contained in:
Lars 2026-03-26 12:55:53 +01:00
parent e799edbae4
commit da803da816
2 changed files with 81 additions and 36 deletions

View File

@ -858,17 +858,41 @@ async def execute_unified_prompt(
stages_debug = result['debug'].get('stages', []) stages_debug = result['debug'].get('stages', [])
# First, collect stage outputs (outputs from base prompts in each stage) # First, collect stage outputs (outputs from base prompts in each stage)
stage_outputs = {} stage_outputs = {} # Raw stage outputs (for expert mode)
extracted_values = {} # Individual values extracted from JSON outputs (for normal mode)
for stage_debug in stages_debug: for stage_debug in stages_debug:
stage_num = stage_debug.get('stage', 0) stage_num = stage_debug.get('stage', 0)
stage_output = stage_debug.get('output', {}) stage_output = stage_debug.get('output', {})
if isinstance(stage_output, dict): if isinstance(stage_output, dict):
for output_key, output_value in stage_output.items(): for output_key, output_value in stage_output.items():
# Store stage outputs (e.g., stage_1_body) # Store raw stage output (for expert mode)
placeholder_key = f"stage_{stage_num}_{output_key}" placeholder_key = f"stage_{stage_num}_{output_key}"
stage_outputs[placeholder_key] = output_value stage_outputs[placeholder_key] = output_value
# Collect all resolved placeholders from prompts # If output is a dict/object, extract individual fields
if isinstance(output_value, dict):
for field_key, field_value in output_value.items():
# Store individual field (for normal mode)
# Use just the field name as key (e.g., "bmi" instead of "stage_1_body.bmi")
# This allows deduplication if multiple stages have the same field
if field_key not in extracted_values:
extracted_values[field_key] = {
'value': field_value if isinstance(field_value, str) else json.dumps(field_value, ensure_ascii=False),
'source_stage': stage_num,
'source_output': output_key
}
# Add extracted values from stage outputs (individual fields)
for field_key, field_data in extracted_values.items():
if field_key not in metadata['placeholders']:
metadata['placeholders'][field_key] = {
'value': field_data['value'],
'description': f"Aus Stage {field_data['source_stage']} ({field_data['source_output']})",
'is_extracted': True # Mark as extracted for filtering
}
# Collect all resolved placeholders from prompts (input placeholders)
for stage_debug in stages_debug: for stage_debug in stages_debug:
for prompt_debug in stage_debug.get('prompts', []): for prompt_debug in stage_debug.get('prompts', []):
resolved_keys = [] resolved_keys = []
@ -883,9 +907,10 @@ async def execute_unified_prompt(
# Get value: first try stage outputs, then cleaned_values # Get value: first try stage outputs, then cleaned_values
value = stage_outputs.get(key, cleaned_values.get(key, '')) value = stage_outputs.get(key, cleaned_values.get(key, ''))
# For stage output placeholders, add special description # For stage output placeholders (raw JSON), add special description
if key.startswith('stage_'): if key.startswith('stage_'):
desc = f"Output aus Stage {key.split('_')[1]} (Basis-Analyse)" desc = f"Rohdaten Stage {key.split('_')[1]} (Basis-Analyse JSON)"
is_stage_raw = True
else: else:
# Find description in catalog # Find description in catalog
desc = None desc = None
@ -895,10 +920,12 @@ async def execute_unified_prompt(
desc = matching[0].get('description', '') desc = matching[0].get('description', '')
break break
desc = desc or '' desc = desc or ''
is_stage_raw = False
metadata['placeholders'][key] = { metadata['placeholders'][key] = {
'value': value if isinstance(value, str) else json.dumps(value, ensure_ascii=False), 'value': value if isinstance(value, str) else json.dumps(value, ensure_ascii=False),
'description': desc 'description': desc,
'is_stage_raw': is_stage_raw # Mark raw stage outputs for expert mode
} }
# Save to database with metadata # Save to database with metadata

View File

@ -34,11 +34,15 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
const metadata = metadataRaw const metadata = metadataRaw
const allPlaceholders = placeholdersRaw const allPlaceholders = placeholdersRaw
// Filter placeholders: In normal mode, hide empty values // Filter placeholders: In normal mode, hide empty values and raw stage outputs
const placeholders = expertMode const placeholders = expertMode
? allPlaceholders ? allPlaceholders
: Object.fromEntries( : Object.fromEntries(
Object.entries(allPlaceholders).filter(([key, data]) => { Object.entries(allPlaceholders).filter(([key, data]) => {
// Hide raw stage outputs (JSON) in normal mode
if (data.is_stage_raw) return false
// Hide empty values
const val = data.value || '' const val = data.value || ''
return val.trim() !== '' && val !== 'nicht verfügbar' && val !== '[Nicht verfügbar]' return val.trim() !== '' && val !== 'nicht verfügbar' && val !== '[Nicht verfügbar]'
}) })
@ -148,15 +152,25 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(placeholders).map(([key, data]) => ( {Object.entries(placeholders).map(([key, data]) => {
<tr key={key} style={{ borderBottom: '1px solid var(--border)' }}> const isExtracted = data.is_extracted
const isStageRaw = data.is_stage_raw
return (
<tr key={key} style={{
borderBottom: '1px solid var(--border)',
background: isStageRaw && expertMode ? 'var(--surface)' : 'transparent'
}}>
<td style={{ <td style={{
padding: '6px 8px', padding: '6px 8px',
fontFamily: 'monospace', fontFamily: 'monospace',
color: 'var(--accent)', color: isStageRaw ? 'var(--text3)' : (isExtracted ? '#6B8E23' : 'var(--accent)'),
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
verticalAlign: 'top' verticalAlign: 'top',
fontSize: isStageRaw ? 10 : 11
}}> }}>
{isExtracted && '↳ '}
{isStageRaw && '🔬 '}
{key} {key}
</td> </td>
<td style={{ <td style={{
@ -164,7 +178,9 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
fontFamily: 'monospace', fontFamily: 'monospace',
wordBreak: 'break-word', wordBreak: 'break-word',
maxWidth: '400px', maxWidth: '400px',
verticalAlign: 'top' verticalAlign: 'top',
fontSize: isStageRaw ? 9 : 11,
color: isStageRaw ? 'var(--text3)' : 'var(--text1)'
}}> }}>
{data.value} {data.value}
</td> </td>
@ -172,12 +188,14 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
padding: '6px 8px', padding: '6px 8px',
color: 'var(--text3)', color: 'var(--text3)',
fontSize: 10, fontSize: 10,
verticalAlign: 'top' verticalAlign: 'top',
fontStyle: isExtracted ? 'italic' : 'normal'
}}> }}>
{data.description || '—'} {data.description || '—'}
</td> </td>
</tr> </tr>
))} )
})}
</tbody> </tbody>
</table> </table>
</div> </div>