| """ |
| Intent parsing β prompt β structured plan. |
| |
| Phase 1 implementation: deterministic heuristic. Scans the prompt |
| for length hints, branch-count hints, topic keywords, then stitches |
| a full Intent from the matching PlanningPreset. |
| |
| Phase 2: swap in ``backend/app/openai_compat_endpoint.run_turn`` |
| (or any LLM) for topic extraction β the ``parse_prompt`` signature |
| stays stable so downstream modules don't change. |
| """ |
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass, field |
| from typing import Any, Dict, List, Optional |
|
|
| from ..config import InteractiveConfig |
| from .audience import Audience, resolve_audience |
| from .presets import PlanningPreset, get_preset |
|
|
|
|
| @dataclass(frozen=True) |
| class Intent: |
| """Fully-resolved planner output. |
| |
| Everything downstream (branching/builder, script/generator) |
| works purely from this struct β no re-parsing of the prompt. |
| """ |
|
|
| prompt: str |
| mode: str |
| objective: str |
| audience: Audience |
| topic: str = "" |
| branch_count: int = 3 |
| depth: int = 3 |
| scenes_per_branch: int = 3 |
| success_metric: str = "" |
| seed_intents: List[str] = field(default_factory=list) |
| scheme: str = "xp_level" |
| raw_hints: Dict[str, Any] = field(default_factory=dict) |
|
|
|
|
| |
| |
| |
|
|
| _BRANCH_HINT = re.compile(r"(\d+)\s*(?:branches|paths|choices)", re.I) |
| _DEPTH_HINT = re.compile(r"(\d+)\s*(?:steps|scenes|levels)\s*deep", re.I) |
| _SCENES_HINT = re.compile(r"(\d+)\s*(?:scenes|clips)(?!\s*deep)", re.I) |
| _TOPIC_HINT = re.compile(r"(?:about|teach|explain|demonstrate)\s+([a-zA-Z][\w\s\-]{1,60}?)(?:[,\.]|$)", re.I) |
| _METRIC_HINT = re.compile(r"(?:until|so that|so)\s+(?:the|they|user)\s+(.{5,60}?)(?:[,\.]|$)", re.I) |
|
|
|
|
| def _extract_int(prompt: str, pattern: re.Pattern[str], default: int, max_: int) -> int: |
| m = pattern.search(prompt) |
| if not m: |
| return default |
| try: |
| v = int(m.group(1)) |
| return max(1, min(v, max_)) |
| except (TypeError, ValueError): |
| return default |
|
|
|
|
| def _extract_topic(prompt: str) -> str: |
| m = _TOPIC_HINT.search(prompt) |
| if not m: |
| return "" |
| return m.group(1).strip().strip('"\'') |
|
|
|
|
| def _extract_metric(prompt: str) -> str: |
| m = _METRIC_HINT.search(prompt) |
| if not m: |
| return "" |
| return m.group(1).strip().rstrip('.,') |
|
|
|
|
| |
| |
| |
|
|
| def parse_prompt( |
| prompt: str, |
| *, |
| cfg: InteractiveConfig, |
| mode: str = "sfw_general", |
| audience_hints: Optional[Dict[str, Any]] = None, |
| ) -> Intent: |
| """Turn a free-text prompt into a fully-resolved Intent. |
| |
| Raises |
| ------ |
| ValueError |
| If ``mode`` has no matching preset AND no sane default. |
| """ |
| preset: Optional[PlanningPreset] = get_preset(mode) or get_preset("sfw_general") |
| if preset is None: |
| raise ValueError(f"No planning preset for mode '{mode}'") |
|
|
| audience = resolve_audience(prompt, explicit=audience_hints, default_language="en") |
|
|
| branch_count = _extract_int(prompt, _BRANCH_HINT, preset.default_branch_count, cfg.max_branches) |
| depth = _extract_int(prompt, _DEPTH_HINT, preset.default_depth, cfg.max_depth) |
| scenes_per_branch = _extract_int(prompt, _SCENES_HINT, preset.default_scenes_per_branch, 20) |
| topic = _extract_topic(prompt) |
| metric = _extract_metric(prompt) |
|
|
| |
| |
| |
| est_nodes = branch_count * depth * max(1, scenes_per_branch) |
| while est_nodes > cfg.max_nodes_per_experience and branch_count > 1: |
| branch_count -= 1 |
| est_nodes = branch_count * depth * max(1, scenes_per_branch) |
|
|
| objective = preset.objective_template |
| if topic: |
| objective = objective.replace("{topic}", topic) |
| objective = objective.replace("{level}", audience.level) |
|
|
| return Intent( |
| prompt=prompt, |
| mode=mode, |
| objective=objective, |
| audience=audience, |
| topic=topic, |
| branch_count=branch_count, |
| depth=depth, |
| scenes_per_branch=scenes_per_branch, |
| success_metric=metric, |
| seed_intents=list(preset.seed_intents), |
| scheme=preset.default_scheme, |
| raw_hints={"preset": preset.mode, "topology": preset.default_topology}, |
| ) |
|
|