mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-04 10:52:17 +02:00
* chore: improve upon mcp prompts * Update api/mcp_server/instructions.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
"""Topic registry + briefing resolver.
|
|
|
|
Stage briefings are *generated* from the registered atoms; they are
|
|
never hand-edited. That guarantees lenses, content, and signals stay
|
|
in lock-step with their canonical topic file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from api.services.voice_prompting_guide._base import (
|
|
Stage,
|
|
VoicePromptingTopic,
|
|
)
|
|
from api.services.voice_prompting_guide.topics import (
|
|
call_flow_design,
|
|
common_guideliines,
|
|
end_call_logic,
|
|
guardrails,
|
|
instruction_collision,
|
|
success_criteria,
|
|
tool_calls,
|
|
turn_taking,
|
|
)
|
|
|
|
_TOPICS: dict[str, VoicePromptingTopic] = {}
|
|
|
|
|
|
def _register(topic: VoicePromptingTopic) -> None:
|
|
if topic.id in _TOPICS:
|
|
raise ValueError(
|
|
f"Duplicate voice-prompting topic id: {topic.id!r}. "
|
|
f"Each atom must be registered exactly once."
|
|
)
|
|
_TOPICS[topic.id] = topic
|
|
|
|
|
|
# Registration order is the briefing display order.
|
|
_register(common_guideliines.TOPIC)
|
|
_register(guardrails.TOPIC)
|
|
_register(call_flow_design.TOPIC)
|
|
_register(tool_calls.TOPIC)
|
|
_register(success_criteria.TOPIC)
|
|
_register(end_call_logic.TOPIC)
|
|
_register(turn_taking.TOPIC)
|
|
_register(instruction_collision.TOPIC)
|
|
|
|
|
|
_STAGE_INTROS: dict[Stage, str] = {
|
|
Stage.plan: (
|
|
"Plan stage. First extract the business context: what the caller must "
|
|
"provide, what the agent must decide, and which policies constrain the "
|
|
"call. Ask the builder for company details, missing domain rules, eligibility or "
|
|
"disconnect conditions, and details only they know; for a rental agent "
|
|
"that might include vehicle type, rental length, trip type, start date, "
|
|
"distance, insurance, deposit method, qualification rules, and whether "
|
|
"one-way rentals are allowed. Decide the persona, call goal, **minimal** "
|
|
"ordered node list, edges, exit conditions, and required tools or "
|
|
"credentials. Do not draft prompts yet; keep the first version simple "
|
|
"and remove scope that does not serve the call goal. You must think and "
|
|
"come up with a plan and interactively refine it with user before moving "
|
|
"to create stage. Interactivity is the key - to be able to gather context "
|
|
"from the user. Its an art and a matter of taste."
|
|
),
|
|
Stage.create: (
|
|
"Create stage. Turn the plan into prompts and SDK TypeScript. Build "
|
|
"nodes around the information the call must capture, grouping related "
|
|
"fields into one node when that keeps the conversation natural. Make "
|
|
"transition instructions explicit: if an edge is labeled 'Move to "
|
|
"Rental Details', the prompt should tell the agent when to call the "
|
|
"matching tool, such as 'move_to_rental_details'. For each node type, "
|
|
"call get_node_type to learn its property schema before emitting it. "
|
|
"When writing a globalNode, also call "
|
|
"get_voice_prompting_guide(topic='common_guidelines') and place that "
|
|
"content in the global node as close to verbatim as possible, adapting "
|
|
"only details the builder has changed."
|
|
),
|
|
Stage.review: (
|
|
"Review stage. Check that the workflow captures the information the "
|
|
"builder wanted and that each prompt names the conditions for moving "
|
|
"to the next node. Read prompts for global-vs-node instruction "
|
|
"collisions, missing handoff cues, and transitions that depend on "
|
|
"unstated business rules. For a globalNode, compare against "
|
|
"get_voice_prompting_guide(topic='common_guidelines') and restore its "
|
|
"structure unless the builder explicitly changed it."
|
|
),
|
|
}
|
|
|
|
|
|
def list_topic_index() -> list[dict[str, str]]:
|
|
"""Flat index of every topic — used when the caller passes no args."""
|
|
return [{"id": t.id, "title": t.title} for t in _TOPICS.values()]
|
|
|
|
|
|
def get_topic(topic_id: str) -> Optional[VoicePromptingTopic]:
|
|
return _TOPICS.get(topic_id)
|
|
|
|
|
|
def build_briefing(
|
|
stage: Stage,
|
|
node_type: Optional[str] = None,
|
|
) -> dict:
|
|
"""Assemble the stage briefing: intro + relevant topics with lenses.
|
|
|
|
A topic is included when (a) its stage lens is marked relevant, and
|
|
(b) its `applies_to_node_types` either is empty (cross-cutting) or
|
|
includes `node_type`. Topics are returned in registration order so
|
|
the same call yields a stable response.
|
|
"""
|
|
topics = [
|
|
t
|
|
for t in _TOPICS.values()
|
|
if t.lens_for(stage) is not None and t.is_relevant_to(node_type)
|
|
]
|
|
|
|
out: dict = {
|
|
"stage": stage.value,
|
|
"intro": _STAGE_INTROS[stage],
|
|
"topics": [t.to_briefing_dict(stage) for t in topics],
|
|
"drill_in": (
|
|
"Call get_voice_prompting_guide(topic='<id>') for the full content "
|
|
"of any topic that materially shapes the prompt you're writing."
|
|
),
|
|
}
|
|
if node_type is not None:
|
|
out["filtered_to_node_type"] = node_type
|
|
return out
|