"""MCP tool that surfaces voice-prompting guidance to the workflow-authoring LLM. The guide is split into stages (plan / create / review) and atoms (topics). Stage calls return a tight briefing — an intro plus a list of relevant topics with one-line lenses. Topic calls return the full reference content for one atom. No-arg calls return a flat index. The LLM is expected to read the briefing for the current stage first, then drill into specific topics only when complexity warrants it. The authoritative guidance lives in `api.services.voice_prompting_guide`; this tool is a thin MCP-facing projection. """ from __future__ import annotations from typing import Any, Optional from fastapi import HTTPException from api.mcp_server.auth import authenticate_mcp_request from api.mcp_server.tracing import traced_tool from api.services.voice_prompting_guide import ( Stage, build_briefing, get_topic, list_topic_index, ) @traced_tool async def get_voice_prompting_guide( stage: Optional[str] = None, topic: Optional[str] = None, node_type: Optional[str] = None, ) -> dict[str, Any]: """Fetch staged voice-prompting guidance for authoring Dograh workflows. Call this BEFORE composing or revising any prompt field on a node. The guide is the authoritative source for prompt-authoring craft (global guidelines, turn-taking, tool calls, success criteria, guardrails); product-mechanics questions (how a node type works at runtime) belong in `search_docs` / `read_doc`. Args: stage: "plan" | "create" | "review". Returns a stage briefing — a short intro plus the list of topics relevant at this stage, each with a one-line lens. Combine with `node_type` during the create stage to narrow to topics that apply to that node type's prompts (e.g. `node_type="agent"`). topic: A topic id from a prior briefing. Returns the full content for that atom. Use after the briefing flags a topic worth drilling into. Mutually exclusive with `stage`. node_type: Optional filter. Most useful with `stage="create"`. Returns: - With `topic`: { id, title, severity, content, stages_relevant, applies_to_node_types?, cross_refs? }. - With `stage`: { stage, intro, topics: [{id, title, lens}], drill_in, filtered_to_node_type? }. - With no args: { topics: [{id, title}], next }. Briefings are designed to be cheap — read the lens, decide what to drill into, then ask for full content for the 1–3 topics that matter for the prompt you're about to write. Always drill into topic="common_guidelines" before writing or revising a globalNode so the template content is actually read. Do not pull every topic. """ await authenticate_mcp_request() if topic is not None and stage is not None: raise ValueError( "Pass either `topic` or `stage`, not both. Use `stage` for a " "briefing index; use `topic` for full content of one atom." ) if topic is not None: atom = get_topic(topic) if atom is None: available = ", ".join(t["id"] for t in list_topic_index()) raise HTTPException( status_code=404, detail=( f"Unknown voice-prompting topic: {topic!r}. " f"Available topics: {available or '(none registered)'}." ), ) return atom.to_deep_dict() if stage is not None: try: stage_enum = Stage(stage) except ValueError: raise HTTPException( status_code=400, detail=( f"Unknown stage: {stage!r}. " f"Use one of: {', '.join(s.value for s in Stage)}." ), ) return build_briefing(stage_enum, node_type=node_type) return { "topics": list_topic_index(), "next": ( "Call with stage='plan'|'create'|'review' for a briefing, or " "topic= for the full content of one atom." ), }