strategies — BaseStrategy and Seven Concrete Strategies

On this page

Each strategy knows how to arrange system instructions, context, memory, and the user query into a specific prompt shape.

Table of contents


BaseStrategy

Python · 10 lines
class BaseStrategy(ABC):
    STRATEGY: PromptStrategy  # set by each subclass

    def build(self, request, context_block, memory_block, guardrail_block, citation_map) -> BuiltPrompt: ...

    @abstractmethod
    def _build_system(self, request, guardrail_block) -> str: ...

    @abstractmethod
    def _build_user(self, request, context_block, memory_block) -> str: ...

build() — internal workflow

  1. system = self._build_system(request, guardrail_block)
  2. user = self._build_user(request, context_block, memory_block)
  3. messages = self._assemble_messages(request, system, user) (built once, then rebuilt after optimization — see step 5)
  4. Construct a PromptOptimizer() with default settings (strategies do not forward any optimizer configuration from the request) and call opt.optimize(system, user, request) → (system, user, saved, notes).
  5. Rebuild messages from the optimized system/user (the pre-optimization messages from step 3 is discarded/overwritten).
  6. Estimate tokens via the local _estimate_tokens() helper (same words × 1.3 heuristic as elsewhere).
  7. Return a BuiltPrompt with documents_included = len(request.documents) — note this is the raw requested document count, not the post-filter/dedup count; PromptBuilder.build() overwrites this field afterward with the accurate post-processing count (see builder.md).

_assemble_messages()

Python
def _assemble_messages(self, request, system, user) -> List[Message]

Builds [Message("system", system)] + request.memory[-10:] + [Message("user", user)]. This means the last 10 raw Message objects from request.memory are injected directly as conversation turns in addition to whatever memory_block text was already woven into the system/user prompt by _build_system/_build_user — conversation history can appear twice in the final messages list (once as formatted text, once as native message turns), though system_prompt/user_prompt (the flat strings) only contain the formatted text version.

Shared helpers available to subclasses

  • _lang(request) -> Lang — resolve_lang(request.language)
  • _tpl(request) -> StrategyTemplate — get_strategy_template(self.STRATEGY)
  • _persona_line(request) -> str — persona_line(self._lang(request), request.user_profile)
  • _language_line(request) -> str — returns "" for empty/"en"/"english" language or when _lang(request) == Lang.AR; otherwise returns f"Respond in {request.language}." (covers languages with no native template, e.g. French/German)

Thread safety

Each concrete strategy (SimpleStrategy(), etc.) is instantiated once at module import time and stored in STRATEGY_REGISTRY — the same instance is reused across every build() call system-wide. Since strategy instances hold no per-call mutable state (all data flows through method arguments), this singleton pattern is safe for concurrent read-only use, though a fresh PromptOptimizer() is constructed on every build() call rather than being a shared/cached instance.


The seven strategies

Each subclass implements only _build_system and _build_user; the assembly/optimization pipeline in BaseStrategy.build() is shared by all of them.

SimpleStrategy

STRATEGY = PromptStrategy.SIMPLE. Direct single-hop QA.

  • System: intro line + persona line (if any) + language note (if any) + guardrail block (if any), joined by blank lines.
  • User: ## Context (if any) → ## Conversation History (if any) → ## Question → ## Answer (empty header, prompting the model to fill it in).
Python
prompt = engine.build(query="What is RAG?", strategy="simple")

ChainOfThoughtStrategy

STRATEGY = PromptStrategy.CHAIN_OF_THOUGHT (enum value "cot"). Explicit reasoning before the final answer.

  • System: intro + tpl.get_extra(lang) (an extra "think step by step" instruction line) + persona + language note + guardrails.
  • User: ## Context → ## Conversation History → ## Question → ## Reasoning Process (with a 4-point scaffold: what is being asked, what's relevant, any conflicts/gaps, most accurate answer) → ## Final Answer.
Python
prompt = engine.build(query="Why did the crisis happen?", strategy="cot")

MultiHopStrategy

STRATEGY = PromptStrategy.MULTI_HOP. Decompose → sub-answer → synthesize, for complex/multi-document questions.

  • System: intro + extra instructions + persona + language note + guardrails.
  • User: ## Context Documents → ## Conversation History → ## Main Question → ## Analysis (scaffold block) → ## Final Answer.
Python · 5 lines
prompt = engine.build(
    query="How are the causes of the 2008 crisis and the 1929 crash related?",
    documents=docs,
    strategy="multi_hop",
)

SelfConsistentStrategy

STRATEGY = PromptStrategy.SELF_CONSISTENT. Asks the model to generate multiple independent reasoning paths and converge on the most consistent answer — the convergence itself happens inside a single generation (via the ## Task scaffold instructing this), not by the library issuing multiple LLM calls and voting; there is no sampling-and-aggregation logic anywhere in this codebase.

  • System: intro + language note + guardrails (no persona line — the only strategy that omits it).
  • User: ## Context → ## Question → ## Task (scaffold) → ## Final Answer. No ## Conversation History section — memory is not surfaced in the user prompt for this strategy even if memory_block is non-empty (it's simply never referenced in _build_user).

[!WARNING] Memory is silently dropped for SelfConsistentStrategy and StepBackStrategy Both strategies' _build_user methods never read the mem parameter passed in. If you rely on conversation history and select self_consistent or step_back, the formatted memory text will not appear in user_prompt (though the raw last-10 Message objects still get appended to messages via _assemble_messages, per the note above).

StepBackStrategy

STRATEGY = PromptStrategy.STEP_BACK. Generates a higher-level abstract question first, then grounds the specific answer in it.

  • System: intro + language note + guardrails.
  • User: ## Context → ## Specific Question → scaffold block (tpl.block(lang, 'steps')) → ## Final Answer. Memory also silently dropped (see warning above).

ReActStrategy

STRATEGY = PromptStrategy.REACT. Reasoning + Acting loop for agentic/tool-use tasks.

  • System: intro + extra instructions + guardrails. No persona line and no language note appended — the only strategy that skips both.
  • User: ## Available Info (context, if any) → ## Available Tools (only if request.extra.get("tools") is truthy — rendered via the module-level _format_tools() helper) → ## Prior Conversation (memory, if any) → ## Task (the query) → a bare Thought label with no content after it (the model is expected to continue generating from there).

_format_tools(tools: List[Dict]) -> str renders each tool as - **{name}**: {description} plus, if the tool dict has a parameters key, an indented Parameters: {compact JSON} line.

Python · 5 lines
prompt = engine.build(
    query="Book a flight to Cairo for next Tuesday.",
    prompt_type="agent",
    extra={"tools": [{"name": "search_flights", "description": "Find flights", "parameters": {"origin": "string", "destination": "string"}}]},
)

LeastToMostStrategy

STRATEGY = PromptStrategy.LEAST_TO_MOST. Solve simpler sub-problems first, then build to the full answer — suited to multi-step math/logic questions.

  • System: intro + language note + guardrails.
  • User: ## Context → ## Problem → ## Solution (scaffold block). Memory also silently dropped (see warning above — LeastToMostStrategy._build_user never reads mem either).

STRATEGY_REGISTRY

Python · 9 lines
STRATEGY_REGISTRY: Dict[PromptStrategy, BaseStrategy] = {
    PromptStrategy.SIMPLE:          SimpleStrategy(),
    PromptStrategy.CHAIN_OF_THOUGHT: ChainOfThoughtStrategy(),
    PromptStrategy.MULTI_HOP:       MultiHopStrategy(),
    PromptStrategy.SELF_CONSISTENT: SelfConsistentStrategy(),
    PromptStrategy.STEP_BACK:       StepBackStrategy(),
    PromptStrategy.REACT:           ReActStrategy(),
    PromptStrategy.LEAST_TO_MOST:  LeastToMostStrategy(),
}

A module-level dict of singleton instances, one per PromptStrategy enum member — every member has an implementation; there is no unmapped/fallback-only strategy.

Extending it: to add a new strategy, subclass BaseStrategy, implement _build_system/_build_user, add a new member to PromptStrategy (in types), add a corresponding StrategyTemplate (in templates), and register the instance in this dict. There is no dynamic/plugin-based registration mechanism — it's a hardcoded module-level dict.

get_strategy()

Python · 6 lines
def get_strategy(strategy: PromptStrategy) -> BaseStrategy:
    impl = STRATEGY_REGISTRY.get(strategy)
    if impl is None:
        logger.warning("[Strategy] Unknown strategy '%s', falling back to Simple.", strategy)
        return STRATEGY_REGISTRY[PromptStrategy.SIMPLE]
    return impl

Never raises — an unregistered strategy value logs a warning and silently falls back to SimpleStrategy. In practice this fallback path is unreachable through the normal PromptEngine.build() flow, since strategy is always coerced to a valid PromptStrategy enum member (or a ValueError is raised) before get_strategy is ever called — but it is reachable if you call get_strategy() directly with something that isn't a real registry key (e.g. None, or a string instead of an enum).

  • builder — the only caller of get_strategy()
  • templates — StrategyTemplate, get_strategy_template, persona_line, resolve_lang
  • optimizer — invoked internally by BaseStrategy.build()
  • types — the return type of every strategy's build()

Related pages