Skip to content

Commit 06bba64

Browse files
ryaneggzclaude
andcommitted
feat: planner agent backend — PlannerConfig schema, system prompt, planner utility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: ryaneggz <kre8mymedia@gmail.com>
1 parent 2f66fb1 commit 06bba64

4 files changed

Lines changed: 119 additions & 0 deletions

File tree

backend/src/schemas/entities/llm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SystemMessage,
1919
ToolMessage,
2020
)
21+
from src.schemas.entities.planner import PlannerConfig
2122
from src.services.prompt.defaults import get_default_system_prompt
2223
from src.utils.format import slugify
2324

@@ -110,6 +111,7 @@ def validate_system_prompt_or_instructions(self):
110111
default_factory=dict,
111112
description="File system storage for the assistant. Key is the file path, value is the file content.",
112113
)
114+
planner: Optional[PlannerConfig] = Field(default=None, description="Planner agent configuration")
113115
metadata: dict = {}
114116
updated_at: Optional[datetime] = None
115117
created_at: Optional[datetime] = None
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Literal, Optional
2+
3+
from pydantic import BaseModel
4+
5+
6+
class PlannerConfig(BaseModel):
7+
"""Configuration for the planner pre-execution phase."""
8+
9+
enabled: bool = False
10+
auto_approve: bool = True # False = pause for user approval via HITL
11+
model: Optional[str] = None # Override model for planner (e.g., use Opus)
12+
scope_level: Literal["conservative", "ambitious"] = "ambitious"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
You are a product planning agent. Your job is to expand a short user prompt into a structured product specification.
2+
3+
## Your Role
4+
5+
- Be ambitious about scope — include features the user didn't explicitly request but would clearly benefit from
6+
- Stay high-level: product context, feature list, user stories, design direction
7+
- Do NOT specify granular implementation details (file names, function signatures, database schemas)
8+
- Identify opportunities to weave AI-powered features into the product
9+
- Think about what would make this product genuinely impressive, not just functional
10+
11+
## Output Format
12+
13+
Structure your plan as markdown with these sections:
14+
15+
### Overview
16+
A 2-3 sentence summary of what we're building and why.
17+
18+
### Features
19+
A numbered list of features, each with:
20+
- Feature name
21+
- One-line description
22+
- 1-2 user stories in "As a [user], I want [action] so that [benefit]" format
23+
24+
### Design Direction
25+
- Visual style and tone
26+
- Key UX principles
27+
- Reference points or inspirations
28+
29+
### Technical Considerations
30+
- Recommended tech stack (if relevant)
31+
- Key architectural decisions
32+
- Performance or scale considerations
33+
34+
### Success Criteria
35+
- How we'll know this is done and done well
36+
- Key metrics to track
37+
38+
Keep the plan concise but comprehensive. Aim for 300-800 words.

backend/src/utils/planner.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Planner pre-execution phase — expands short prompts into structured product specs."""
2+
3+
from pathlib import Path
4+
5+
from langchain.chat_models import init_chat_model
6+
from langchain_core.messages import HumanMessage, SystemMessage
7+
8+
from src.schemas.entities.planner import PlannerConfig
9+
from src.utils.llm import resolve_api_key
10+
from src.utils.logger import logger
11+
12+
# Load the planner system prompt
13+
_PLANNER_PROMPT_PATH = Path(__file__).parent.parent / "static" / "prompts" / "md" / "planner.md"
14+
_PLANNER_PROMPT = ""
15+
if _PLANNER_PROMPT_PATH.exists():
16+
_PLANNER_PROMPT = _PLANNER_PROMPT_PATH.read_text()
17+
18+
19+
async def run_planner(
20+
user_message: str,
21+
planner_config: PlannerConfig,
22+
default_model: str,
23+
api_key: str | None = None,
24+
user_keys: dict[str, str] | None = None,
25+
) -> str:
26+
"""Run the planner to expand a short prompt into a structured spec.
27+
28+
Returns the plan as markdown text.
29+
"""
30+
model_name = planner_config.model or default_model
31+
32+
# Resolve API key for the planner model
33+
planner_api_key = api_key
34+
if planner_config.model:
35+
resolved = resolve_api_key(planner_config.model, user_keys)
36+
if resolved:
37+
planner_api_key = resolved
38+
39+
llm = init_chat_model(model_name, api_key=planner_api_key)
40+
41+
scope_instruction = ""
42+
if planner_config.scope_level == "conservative":
43+
scope_instruction = (
44+
"\n\nIMPORTANT: Keep the scope conservative. "
45+
"Only include features explicitly requested by the user. Do not add extras."
46+
)
47+
else:
48+
scope_instruction = (
49+
"\n\nBe ambitious about scope. Include features that would make this product impressive, "
50+
"even if the user didn't explicitly request them."
51+
)
52+
53+
system_prompt = _PLANNER_PROMPT + scope_instruction
54+
55+
messages = [
56+
SystemMessage(content=system_prompt),
57+
HumanMessage(content=f"Please create a product plan for the following request:\n\n{user_message}"),
58+
]
59+
60+
logger.info(f"planner_phase model={model_name} scope={planner_config.scope_level}")
61+
62+
response = await llm.ainvoke(messages)
63+
plan_text = response.content if isinstance(response.content, str) else str(response.content)
64+
65+
logger.info(f"planner_phase_complete plan_length={len(plan_text)}")
66+
67+
return plan_text

0 commit comments

Comments
 (0)