Source code for trader.chat.prompt

"""Building the chat prompts, and parsing what comes back.

Same two rules `trader.llm.prompt` states for the trading decision prompt,
applied to a conversational one:

**Everything that can go wrong becomes a safe, honest string.** A missing
`response` key, a non-string value, an empty reply — none of them raise, and
none of them invent an answer. A misbehaving model must never be presented to
the operator as a confident answer it did not actually give.

**The model is only ever shown data this process already fetched and
verified against the database.** Every builder here takes pre-rendered
context text, never a repository or a session — the same separation
`trader.llm.prompt.build_messages` keeps between "what the model saw" and how
it was fetched, so what reaches the model is exactly what a human reading
`inputs_json` for a trading decision would also see.
"""

__all__ = [
    "CHAT_RESPONSE_SCHEMA",
    "build_chat_messages",
    "build_explain_decision_messages",
    "build_strategy_review_messages",
    "parse_chat_response",
]

CHAT_RESPONSE_SCHEMA: dict[str, object] = {
    "type": "object",
    "properties": {"response": {"type": "string"}},
    "required": ["response"],
}

_GENERAL_SYSTEM = """You are a helpful assistant for an AI-assisted Alpaca \
paper-trading app. You answer a human operator's questions about the app's \
own account, trades, and performance, and general trading questions.

Rules:
- Answer the user's question about THIS account, its positions, its trades, \
or its performance using ONLY the data given below. Never invent a position, \
a price, a trade, or a P/L figure that is not in that data.
- If the data given does not answer the question, say so plainly rather than \
guessing.
- You may answer a general market or trading question using your own \
knowledge, but make clear that is general information, not a live quote or a \
fact about this account.
- You are not placing any order and cannot place one from this conversation.
- You may only respond with the required JSON object.
"""

_EXPLAIN_DECISION_SYSTEM = """You are explaining one specific automated \
trading decision this app already made, to the human operator who runs it.

You are given the EXACT row this app recorded for that decision: the action \
taken, the reasoning the deciding strategy gave, and the raw inputs it was \
shown at the time.

Rules:
- Explain that recorded decision conversationally, in plain English. Ground \
every claim in the reasoning and inputs given below.
- Do NOT re-evaluate the symbol or offer a fresh opinion on what to do now — \
this is a historical explanation of what was recorded and why, not a new \
decision.
- If the recorded reasoning or inputs are sparse or unclear, say so honestly \
rather than filling the gap with invented detail.
- You may only respond with the required JSON object.
"""

_STRATEGY_REVIEW_SYSTEM = """You are reviewing config/strategies.yaml for the \
human operator of an AI-assisted trading app.

You are given the exact contents of that file below.

Rules:
- Explain what is configured: each strategy's id, type, mode (trading vs \
shadow), and parameters, and what they mean.
- You may suggest changes in your response text for the operator to consider.
- You can NEVER edit config/strategies.yaml or any other file yourself, and \
must never claim to have done so or offer to do so directly — v1 is \
read/explain/suggest only. Any change is applied by the operator, by hand.
- You may only respond with the required JSON object.
"""


[docs] def build_chat_messages(message: str, context: str) -> tuple[str, str]: """`(system, user)` for a general Q&A / positions / trades / performance turn.""" user = f"Account data:\n{context}\n\nUser's question: {message}" return _GENERAL_SYSTEM, user
[docs] def build_explain_decision_messages(message: str, decision_text: str) -> tuple[str, str]: """`(system, user)` for explaining one recorded decision.""" user = f"Recorded decision:\n{decision_text}\n\nUser's question: {message}" return _EXPLAIN_DECISION_SYSTEM, user
[docs] def build_strategy_review_messages(message: str, strategies_yaml: str) -> tuple[str, str]: """`(system, user)` for reviewing `config/strategies.yaml`.""" user = f"config/strategies.yaml:\n{strategies_yaml}\n\nUser's question: {message}" return _STRATEGY_REVIEW_SYSTEM, user
[docs] def parse_chat_response(payload: dict[str, object]) -> str: """Turn a model reply into plain text. Never raises. Same discipline as `trader.llm.prompt.parse_decision`: an unusable response degrades to a safe, honest string rather than raising past this boundary or presenting garbage as an answer. """ response = payload.get("response") if isinstance(response, str) and response.strip(): return response.strip() return "The model responded with something that wasn't readable text."