|
| 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