feat: unified prompt executor - Phase 2 complete (Issue #28)
Backend:
- prompt_executor.py: Universal executor for base + pipeline prompts
- Dynamic placeholder resolution
- JSON output validation
- Multi-stage parallel execution (sequential impl)
- Reference and inline prompt support
- Data loading per module (körper, ernährung, training, schlaf, vitalwerte)
Endpoints:
- POST /api/prompts/execute - Execute unified prompts
- POST /api/prompts/unified - Create unified prompts
- PUT /api/prompts/unified/{id} - Update unified prompts
Frontend:
- api.js: executeUnifiedPrompt, createUnifiedPrompt, updateUnifiedPrompt
Next: Phase 3 - Frontend UI consolidation
This commit is contained in:
parent
33653fdfd4
commit
7be7266477
351
backend/prompt_executor.py
Normal file
351
backend/prompt_executor.py
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
"""
|
||||||
|
Unified Prompt Executor (Issue #28 Phase 2)
|
||||||
|
|
||||||
|
Executes both base and pipeline-type prompts with:
|
||||||
|
- Dynamic placeholder resolution
|
||||||
|
- JSON output validation
|
||||||
|
- Multi-stage parallel execution
|
||||||
|
- Reference and inline prompt support
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from db import get_db, get_cursor, r2d
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_placeholders(template: str, variables: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Replace {{placeholder}} with values from variables dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template: String with {{key}} placeholders
|
||||||
|
variables: Dict of key -> value mappings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Template with placeholders replaced
|
||||||
|
"""
|
||||||
|
def replacer(match):
|
||||||
|
key = match.group(1).strip()
|
||||||
|
if key in variables:
|
||||||
|
value = variables[key]
|
||||||
|
# Convert dict/list to JSON string
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
return json.dumps(value, ensure_ascii=False)
|
||||||
|
return str(value)
|
||||||
|
# Keep placeholder if no value found
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
return re.sub(r'\{\{([^}]+)\}\}', replacer, template)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_json_output(output: str, schema: Optional[Dict] = None) -> Dict:
|
||||||
|
"""
|
||||||
|
Validate that output is valid JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: String to validate
|
||||||
|
schema: Optional JSON schema to validate against (TODO: jsonschema library)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON dict
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If output is not valid JSON
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed = json.loads(output)
|
||||||
|
# TODO: Add jsonschema validation if schema provided
|
||||||
|
return parsed
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"AI returned invalid JSON: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_prompt(
|
||||||
|
prompt_slug: str,
|
||||||
|
variables: Dict[str, Any],
|
||||||
|
openrouter_call_func
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute a single prompt (base or pipeline type).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt_slug: Slug of prompt to execute
|
||||||
|
variables: Dict of variables for placeholder replacement
|
||||||
|
openrouter_call_func: Async function(prompt_text) -> response_text
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with execution results:
|
||||||
|
{
|
||||||
|
"type": "base" | "pipeline",
|
||||||
|
"slug": "...",
|
||||||
|
"output": "..." | {...}, # String or parsed JSON
|
||||||
|
"stages": [...] # Only for pipeline type
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Load prompt from database
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT * FROM ai_prompts
|
||||||
|
WHERE slug = %s AND active = true""",
|
||||||
|
(prompt_slug,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, f"Prompt nicht gefunden: {prompt_slug}")
|
||||||
|
|
||||||
|
prompt = r2d(row)
|
||||||
|
|
||||||
|
prompt_type = prompt.get('type', 'pipeline')
|
||||||
|
|
||||||
|
if prompt_type == 'base':
|
||||||
|
# Base prompt: single execution with template
|
||||||
|
return await execute_base_prompt(prompt, variables, openrouter_call_func)
|
||||||
|
|
||||||
|
elif prompt_type == 'pipeline':
|
||||||
|
# Pipeline prompt: multi-stage execution
|
||||||
|
return await execute_pipeline_prompt(prompt, variables, openrouter_call_func)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, f"Unknown prompt type: {prompt_type}")
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_base_prompt(
|
||||||
|
prompt: Dict,
|
||||||
|
variables: Dict[str, Any],
|
||||||
|
openrouter_call_func
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute a base-type prompt (single template)."""
|
||||||
|
template = prompt.get('template')
|
||||||
|
if not template:
|
||||||
|
raise HTTPException(400, f"Base prompt missing template: {prompt['slug']}")
|
||||||
|
|
||||||
|
# Resolve placeholders
|
||||||
|
prompt_text = resolve_placeholders(template, variables)
|
||||||
|
|
||||||
|
# Call AI
|
||||||
|
response = await openrouter_call_func(prompt_text)
|
||||||
|
|
||||||
|
# Validate JSON if required
|
||||||
|
output_format = prompt.get('output_format', 'text')
|
||||||
|
if output_format == 'json':
|
||||||
|
output = validate_json_output(response, prompt.get('output_schema'))
|
||||||
|
else:
|
||||||
|
output = response
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "base",
|
||||||
|
"slug": prompt['slug'],
|
||||||
|
"output": output,
|
||||||
|
"output_format": output_format
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_pipeline_prompt(
|
||||||
|
prompt: Dict,
|
||||||
|
variables: Dict[str, Any],
|
||||||
|
openrouter_call_func
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute a pipeline-type prompt (multi-stage).
|
||||||
|
|
||||||
|
Each stage's results are added to variables for next stage.
|
||||||
|
"""
|
||||||
|
stages = prompt.get('stages')
|
||||||
|
if not stages:
|
||||||
|
raise HTTPException(400, f"Pipeline prompt missing stages: {prompt['slug']}")
|
||||||
|
|
||||||
|
# Parse stages if stored as JSON string
|
||||||
|
if isinstance(stages, str):
|
||||||
|
stages = json.loads(stages)
|
||||||
|
|
||||||
|
stage_results = []
|
||||||
|
context_vars = variables.copy()
|
||||||
|
|
||||||
|
# Execute stages in order
|
||||||
|
for stage_def in sorted(stages, key=lambda s: s['stage']):
|
||||||
|
stage_num = stage_def['stage']
|
||||||
|
stage_prompts = stage_def.get('prompts', [])
|
||||||
|
|
||||||
|
if not stage_prompts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Execute all prompts in this stage (parallel concept, sequential impl for now)
|
||||||
|
stage_outputs = {}
|
||||||
|
|
||||||
|
for prompt_def in stage_prompts:
|
||||||
|
source = prompt_def.get('source')
|
||||||
|
output_key = prompt_def.get('output_key', f'stage{stage_num}')
|
||||||
|
output_format = prompt_def.get('output_format', 'text')
|
||||||
|
|
||||||
|
if source == 'reference':
|
||||||
|
# Reference to another prompt
|
||||||
|
ref_slug = prompt_def.get('slug')
|
||||||
|
if not ref_slug:
|
||||||
|
raise HTTPException(400, f"Reference prompt missing slug in stage {stage_num}")
|
||||||
|
|
||||||
|
# Load referenced prompt
|
||||||
|
result = await execute_prompt(ref_slug, context_vars, openrouter_call_func)
|
||||||
|
output = result['output']
|
||||||
|
|
||||||
|
elif source == 'inline':
|
||||||
|
# Inline template
|
||||||
|
template = prompt_def.get('template')
|
||||||
|
if not template:
|
||||||
|
raise HTTPException(400, f"Inline prompt missing template in stage {stage_num}")
|
||||||
|
|
||||||
|
prompt_text = resolve_placeholders(template, context_vars)
|
||||||
|
response = await openrouter_call_func(prompt_text)
|
||||||
|
|
||||||
|
# Validate JSON if required
|
||||||
|
if output_format == 'json':
|
||||||
|
output = validate_json_output(response, prompt_def.get('output_schema'))
|
||||||
|
else:
|
||||||
|
output = response
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, f"Unknown prompt source: {source}")
|
||||||
|
|
||||||
|
# Store output with key
|
||||||
|
stage_outputs[output_key] = output
|
||||||
|
|
||||||
|
# Add to context for next stage
|
||||||
|
context_vars[f'stage_{stage_num}_{output_key}'] = output
|
||||||
|
|
||||||
|
stage_results.append({
|
||||||
|
"stage": stage_num,
|
||||||
|
"outputs": stage_outputs
|
||||||
|
})
|
||||||
|
|
||||||
|
# Final output is last stage's first output
|
||||||
|
final_output = stage_results[-1]['outputs'] if stage_results else {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "pipeline",
|
||||||
|
"slug": prompt['slug'],
|
||||||
|
"stages": stage_results,
|
||||||
|
"output": final_output,
|
||||||
|
"output_format": prompt.get('output_format', 'text')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_prompt_with_data(
|
||||||
|
prompt_slug: str,
|
||||||
|
profile_id: str,
|
||||||
|
modules: Optional[Dict[str, bool]] = None,
|
||||||
|
timeframes: Optional[Dict[str, int]] = None,
|
||||||
|
openrouter_call_func = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute prompt with data loaded from database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt_slug: Slug of prompt to execute
|
||||||
|
profile_id: User profile ID
|
||||||
|
modules: Dict of module -> enabled (e.g., {"körper": true})
|
||||||
|
timeframes: Dict of module -> days (e.g., {"körper": 30})
|
||||||
|
openrouter_call_func: Async function for AI calls
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Execution result dict
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Build variables from data modules
|
||||||
|
variables = {
|
||||||
|
'profile_id': profile_id,
|
||||||
|
'today': datetime.now().strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load data for enabled modules
|
||||||
|
if modules:
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Weight data
|
||||||
|
if modules.get('körper'):
|
||||||
|
days = timeframes.get('körper', 30)
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, weight FROM weight_log
|
||||||
|
WHERE profile_id = %s AND date >= %s
|
||||||
|
ORDER BY date DESC""",
|
||||||
|
(profile_id, since)
|
||||||
|
)
|
||||||
|
variables['weight_data'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Nutrition data
|
||||||
|
if modules.get('ernährung'):
|
||||||
|
days = timeframes.get('ernährung', 30)
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, kcal, protein_g, fat_g, carbs_g
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id = %s AND date >= %s
|
||||||
|
ORDER BY date DESC""",
|
||||||
|
(profile_id, since)
|
||||||
|
)
|
||||||
|
variables['nutrition_data'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Activity data
|
||||||
|
if modules.get('training'):
|
||||||
|
days = timeframes.get('training', 14)
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, activity_type, duration_min, kcal_active, hr_avg
|
||||||
|
FROM activity_log
|
||||||
|
WHERE profile_id = %s AND date >= %s
|
||||||
|
ORDER BY date DESC""",
|
||||||
|
(profile_id, since)
|
||||||
|
)
|
||||||
|
variables['activity_data'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Sleep data
|
||||||
|
if modules.get('schlaf'):
|
||||||
|
days = timeframes.get('schlaf', 14)
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, sleep_segments, source
|
||||||
|
FROM sleep_log
|
||||||
|
WHERE profile_id = %s AND date >= %s
|
||||||
|
ORDER BY date DESC""",
|
||||||
|
(profile_id, since)
|
||||||
|
)
|
||||||
|
variables['sleep_data'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Vitals data
|
||||||
|
if modules.get('vitalwerte'):
|
||||||
|
days = timeframes.get('vitalwerte', 7)
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# Baseline vitals
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, resting_hr, hrv, vo2_max, spo2, respiratory_rate
|
||||||
|
FROM vitals_baseline
|
||||||
|
WHERE profile_id = %s AND date >= %s
|
||||||
|
ORDER BY date DESC""",
|
||||||
|
(profile_id, since)
|
||||||
|
)
|
||||||
|
variables['vitals_baseline'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Blood pressure
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT measured_at, systolic, diastolic, pulse
|
||||||
|
FROM blood_pressure_log
|
||||||
|
WHERE profile_id = %s AND measured_at >= %s
|
||||||
|
ORDER BY measured_at DESC""",
|
||||||
|
(profile_id, since + ' 00:00:00')
|
||||||
|
)
|
||||||
|
variables['blood_pressure'] = [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Mental/Goals (no timeframe, just current state)
|
||||||
|
if modules.get('mentales') or modules.get('ziele'):
|
||||||
|
# TODO: Add mental state / goals data when implemented
|
||||||
|
variables['goals_data'] = []
|
||||||
|
|
||||||
|
# Execute prompt
|
||||||
|
return await execute_prompt(prompt_slug, variables, openrouter_call_func)
|
||||||
|
|
@ -684,3 +684,209 @@ def reset_prompt_to_default(prompt_id: str, session: dict=Depends(require_admin)
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# UNIFIED PROMPT SYSTEM (Issue #28 Phase 2)
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
from prompt_executor import execute_prompt_with_data
|
||||||
|
from models import UnifiedPromptCreate, UnifiedPromptUpdate
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/execute")
|
||||||
|
async def execute_unified_prompt(
|
||||||
|
prompt_slug: str,
|
||||||
|
modules: Optional[dict] = None,
|
||||||
|
timeframes: Optional[dict] = None,
|
||||||
|
session: dict = Depends(require_auth)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Execute a unified prompt (base or pipeline type).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt_slug: Slug of prompt to execute
|
||||||
|
modules: Dict of enabled modules (e.g., {"körper": true})
|
||||||
|
timeframes: Dict of timeframes per module (e.g., {"körper": 30})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Execution result with outputs
|
||||||
|
"""
|
||||||
|
profile_id = session['profile_id']
|
||||||
|
|
||||||
|
# Use default modules/timeframes if not provided
|
||||||
|
if not modules:
|
||||||
|
modules = {
|
||||||
|
'körper': True,
|
||||||
|
'ernährung': True,
|
||||||
|
'training': True,
|
||||||
|
'schlaf': True,
|
||||||
|
'vitalwerte': True
|
||||||
|
}
|
||||||
|
|
||||||
|
if not timeframes:
|
||||||
|
timeframes = {
|
||||||
|
'körper': 30,
|
||||||
|
'ernährung': 30,
|
||||||
|
'training': 14,
|
||||||
|
'schlaf': 14,
|
||||||
|
'vitalwerte': 7
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute with prompt_executor
|
||||||
|
result = await execute_prompt_with_data(
|
||||||
|
prompt_slug=prompt_slug,
|
||||||
|
profile_id=profile_id,
|
||||||
|
modules=modules,
|
||||||
|
timeframes=timeframes,
|
||||||
|
openrouter_call_func=call_openrouter
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/unified")
|
||||||
|
def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(require_admin)):
|
||||||
|
"""
|
||||||
|
Create a new unified prompt (base or pipeline type).
|
||||||
|
Admin only.
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Check for duplicate slug
|
||||||
|
cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (p.slug,))
|
||||||
|
if cur.fetchone():
|
||||||
|
raise HTTPException(status_code=400, detail="Slug already exists")
|
||||||
|
|
||||||
|
# Validate type
|
||||||
|
if p.type not in ['base', 'pipeline']:
|
||||||
|
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
|
||||||
|
|
||||||
|
# Validate base type has template
|
||||||
|
if p.type == 'base' and not p.template:
|
||||||
|
raise HTTPException(status_code=400, detail="Base prompts require a template")
|
||||||
|
|
||||||
|
# Validate pipeline type has stages
|
||||||
|
if p.type == 'pipeline' and not p.stages:
|
||||||
|
raise HTTPException(status_code=400, detail="Pipeline prompts require stages")
|
||||||
|
|
||||||
|
# Convert stages to JSONB
|
||||||
|
stages_json = None
|
||||||
|
if p.stages:
|
||||||
|
stages_json = json.dumps([
|
||||||
|
{
|
||||||
|
'stage': s.stage,
|
||||||
|
'prompts': [
|
||||||
|
{
|
||||||
|
'source': pr.source,
|
||||||
|
'slug': pr.slug,
|
||||||
|
'template': pr.template,
|
||||||
|
'output_key': pr.output_key,
|
||||||
|
'output_format': pr.output_format,
|
||||||
|
'output_schema': pr.output_schema
|
||||||
|
}
|
||||||
|
for pr in s.prompts
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for s in p.stages
|
||||||
|
])
|
||||||
|
|
||||||
|
prompt_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT INTO ai_prompts
|
||||||
|
(id, slug, name, display_name, description, template, category, active, sort_order,
|
||||||
|
type, stages, output_format, output_schema)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
||||||
|
(
|
||||||
|
prompt_id, p.slug, p.name, p.display_name, p.description,
|
||||||
|
p.template, p.category, p.active, p.sort_order,
|
||||||
|
p.type, stages_json, p.output_format,
|
||||||
|
json.dumps(p.output_schema) if p.output_schema else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"id": prompt_id, "slug": p.slug}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/unified/{prompt_id}")
|
||||||
|
def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict = Depends(require_admin)):
|
||||||
|
"""
|
||||||
|
Update a unified prompt.
|
||||||
|
Admin only.
|
||||||
|
"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Check if exists
|
||||||
|
cur.execute("SELECT id FROM ai_prompts WHERE id=%s", (prompt_id,))
|
||||||
|
if not cur.fetchone():
|
||||||
|
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||||
|
|
||||||
|
# Build update query
|
||||||
|
updates = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if p.name is not None:
|
||||||
|
updates.append('name=%s')
|
||||||
|
values.append(p.name)
|
||||||
|
if p.display_name is not None:
|
||||||
|
updates.append('display_name=%s')
|
||||||
|
values.append(p.display_name)
|
||||||
|
if p.description is not None:
|
||||||
|
updates.append('description=%s')
|
||||||
|
values.append(p.description)
|
||||||
|
if p.type is not None:
|
||||||
|
if p.type not in ['base', 'pipeline']:
|
||||||
|
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
|
||||||
|
updates.append('type=%s')
|
||||||
|
values.append(p.type)
|
||||||
|
if p.category is not None:
|
||||||
|
updates.append('category=%s')
|
||||||
|
values.append(p.category)
|
||||||
|
if p.active is not None:
|
||||||
|
updates.append('active=%s')
|
||||||
|
values.append(p.active)
|
||||||
|
if p.sort_order is not None:
|
||||||
|
updates.append('sort_order=%s')
|
||||||
|
values.append(p.sort_order)
|
||||||
|
if p.template is not None:
|
||||||
|
updates.append('template=%s')
|
||||||
|
values.append(p.template)
|
||||||
|
if p.output_format is not None:
|
||||||
|
updates.append('output_format=%s')
|
||||||
|
values.append(p.output_format)
|
||||||
|
if p.output_schema is not None:
|
||||||
|
updates.append('output_schema=%s')
|
||||||
|
values.append(json.dumps(p.output_schema))
|
||||||
|
if p.stages is not None:
|
||||||
|
stages_json = json.dumps([
|
||||||
|
{
|
||||||
|
'stage': s.stage,
|
||||||
|
'prompts': [
|
||||||
|
{
|
||||||
|
'source': pr.source,
|
||||||
|
'slug': pr.slug,
|
||||||
|
'template': pr.template,
|
||||||
|
'output_key': pr.output_key,
|
||||||
|
'output_format': pr.output_format,
|
||||||
|
'output_schema': pr.output_schema
|
||||||
|
}
|
||||||
|
for pr in s.prompts
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for s in p.stages
|
||||||
|
])
|
||||||
|
updates.append('stages=%s')
|
||||||
|
values.append(stages_json)
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE ai_prompts SET {', '.join(updates)}, updated=CURRENT_TIMESTAMP WHERE id=%s",
|
||||||
|
values + [prompt_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
|
||||||
|
|
@ -305,4 +305,15 @@ export const api = {
|
||||||
|
|
||||||
// Pipeline Execution (Issue #28 Phase 2)
|
// Pipeline Execution (Issue #28 Phase 2)
|
||||||
executePipeline: (configId=null) => req('/insights/pipeline' + (configId ? `?config_id=${configId}` : ''), json({})),
|
executePipeline: (configId=null) => req('/insights/pipeline' + (configId ? `?config_id=${configId}` : ''), json({})),
|
||||||
|
|
||||||
|
// Unified Prompt System (Issue #28 Phase 2)
|
||||||
|
executeUnifiedPrompt: (slug, modules=null, timeframes=null) => {
|
||||||
|
const params = new URLSearchParams({ prompt_slug: slug })
|
||||||
|
const body = {}
|
||||||
|
if (modules) body.modules = modules
|
||||||
|
if (timeframes) body.timeframes = timeframes
|
||||||
|
return req('/prompts/execute?' + params, json(body))
|
||||||
|
},
|
||||||
|
createUnifiedPrompt: (d) => req('/prompts/unified', json(d)),
|
||||||
|
updateUnifiedPrompt: (id,d) => req(`/prompts/unified/${id}`, jput(d)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user