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