File size: 7,343 Bytes
71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | """Prompt construction + a deterministic fallback story.
Design rule: the model only ever **narrates facts we extracted**. Every prompt
hands it the structured truth (roles, symbols, edges) and forbids inventing
files or connections. The per-file MAP summaries are style-neutral (so they
cache across styles); the project REDUCE applies the chosen style + difficulty.
"""
from __future__ import annotations
import posixpath
import config
from schema import FileInfo, FileSummary, ProjectModel, ProjectStory, StorySection
_GROUNDING = (
"You are StoryCode. You explain code to people who are NOT programmers. "
"Only describe what the FACTS below state. Never invent files, functions, or "
"connections that aren't listed. If something isn't in the facts, don't mention it. "
"Use the file's actual function/class names to explain what it does β don't just "
"repeat the role category."
)
# --- MAP: per-file factual summary ------------------------------------------
def file_digest(model: ProjectModel, f: FileInfo) -> str:
role = config.ROLE_LABEL.get(f.role, f.role)
syms = ", ".join(s.name for s in f.symbols[:12]) or "(no named functions)"
dep_names = [posixpath.basename(d) for d in f.depends_on]
deps = ", ".join(dep_names) or "(nothing else in this project)"
used_by = sum(1 for o in model.files if f.path in o.depends_on)
return (
f"FILE: {f.path}\n"
f"Language: {f.lang}\n"
f"Its job-category: {role}\n"
f"Defines: {syms}\n"
f"It uses (depends on): {deps}\n"
f"Number of other files that depend on it: {used_by}\n"
f"Lines of code: {f.loc}"
)
def map_prompt(model: ProjectModel, f: FileInfo) -> list[dict]:
facts = file_digest(model, f)
user = (
f"{facts}\n\n"
"Write a JSON object with:\n"
'- "one_liner": at most 8 words naming this file\'s actual job. '
"Use the function/class names listed above to be specific β "
'e.g. "Defines the PPO actor-critic neural network" not just "The engine".\n'
'- "summary": 2-3 sentences, no jargon, explaining:\n'
" 1) What this file actually does (based on its function/class names)\n"
" 2) Which specific files it connects to and why\n"
"Speak to a non-coder who wants to understand the project."
)
return [{"role": "system", "content": _GROUNDING},
{"role": "user", "content": user}]
# --- REDUCE: the whole-project story ----------------------------------------
def project_digest(model: ProjectModel, summaries: dict[str, FileSummary]) -> str:
lines = [f"PROJECT: {model.name}",
f"Languages: {', '.join(model.languages) or 'mixed'}",
f"Front door (starts here): {', '.join(model.entry_points) or 'unknown'}",
""]
# Group files by role for better context
by_role: dict[str, list[FileInfo]] = {}
for f in model.files:
by_role.setdefault(f.role, []).append(f)
role_order = ["entry", "backend", "data", "config", "util", "frontend", "test", "other"]
for role in role_order:
group = by_role.get(role, [])
if not group:
continue
label = config.ROLE_LABEL.get(role, role)
lines.append(f"--- {label} ---")
for f in group:
base = posixpath.basename(f.path)
one = summaries.get(f.path)
job = one.one_liner if one else config.ROLE_LABEL.get(f.role, f.role)
deps = ", ".join(posixpath.basename(d) for d in f.depends_on) or "nothing internal"
lines.append(f"- {base} β {job}. Uses: {deps}.")
lines.append("")
return "\n".join(lines)
def reduce_prompt(model: ProjectModel, summaries: dict[str, FileSummary],
style_key: str, difficulty_key: str) -> list[dict]:
style = config.STYLE_BY_KEY.get(style_key, config.STYLE_BY_KEY[config.DEFAULT_STYLE])
diff = config.DIFFICULTY_BY_KEY.get(difficulty_key,
config.DIFFICULTY_BY_KEY[config.DEFAULT_DIFFICULTY])
digest = project_digest(model, summaries)
user = (
f"{digest}\n\n"
f"STYLE: {style.voice}\n"
f"AUDIENCE: {diff.voice}\n\n"
"Write a JSON object with:\n"
'- "title": a short, inviting title for this project\'s story.\n'
'- "overview": 2-4 sentences explaining what this project IS (its purpose), '
"WHAT it does (the main capability), and HOW it works (the key architectural "
"flow from entry point through the files it uses). Be specific β use the "
"project name, the key files, and the real data flow.\n"
'- "steps": exactly 3-6 {"heading","body"} sections that follow the REAL '
"execution flow: start at the front door (entry point), then follow the "
"import chain through the files it uses. Each step should explain:\n"
" 1) What happens at this stage\n"
" 2) Which files are involved and what they do\n"
" 3) Why this step matters for the overall project\n"
"Apply the STYLE here. Don't skip intermediate files β the reader should "
"understand the complete journey from start to finish.\n"
'- "plain_overview": the SAME walkthrough but in flat, plain English with NO '
"metaphors (so a nervous user always has a literal version). 3-5 sentences. "
"This should be simple enough for a 12-year-old to understand.\n"
"Stay 100% true to the FILES facts above. Never invent connections."
)
return [{"role": "system", "content": _GROUNDING},
{"role": "user", "content": user}]
# --- Deterministic fallback (no model needed) -------------------------------
def plain_fallback_story(model: ProjectModel,
summaries: dict[str, FileSummary] | None = None) -> ProjectStory:
"""A truthful, model-free story built straight from the facts.
Used if the model endpoint is unavailable, so the app degrades gracefully
instead of showing nothing.
"""
summaries = summaries or {}
entry = model.entry_points[0] if model.entry_points else (
model.files[0].path if model.files else "")
steps: list[StorySection] = []
seen: set[str] = set()
def walk(path: str, depth: int):
f = model.by_path(path)
if f is None or path in seen or depth > 4:
return
seen.add(path)
base = posixpath.basename(path)
one = summaries.get(path)
job = one.summary if one else (
f"This is the {config.ROLE_LABEL.get(f.role, f.role).lower()} of the project.")
steps.append(StorySection(heading=base, body=job))
for dep in f.depends_on:
walk(dep, depth + 1)
if entry:
walk(entry, 0)
for f in model.files: # include anything not reachable from the entry
walk(f.path, 0)
langs = ", ".join(model.languages) or "code"
overview = (f"{model.name} is a {langs} project made of {len(model.files)} files. "
f"It starts at {posixpath.basename(entry) if entry else 'its main file'}, "
"which then uses the other files to do its work.")
return ProjectStory(title=f"The story of {model.name}", overview=overview,
steps=steps[:8], plain_overview=overview)
|