SOUL: The system prompt

Before the code: what identity and tools mean for an assistant

Right now your assistant is a smart parrot. It answers anything in any tone. To turn it into someone, you give it an identity: a short paragraph at the start of every conversation that says who it is, who it is talking to, and how it should behave. Then to turn it from a chatter into a doer, you give it tools: normal Python functions like read_file or run_command that the model can ask you to run on its behalf. Identity tells the assistant what to be. Tools let it do real work.

Identity and tools in one picture

The identity goes in at the top of every turn. Tools are functions the model can call by name.

Quiz: Quiz

Loading practice…

Right now the bot has no personality. It will say anything, in any tone, with any pronoun. The fix is a system prompt: a special first message that the model reads as instructions instead of conversation. We call ours SOUL.

03-personality-soul/SOUL.md
markdown
# OpenClaw

You are OpenClaw, a personal AI assistant.

## Personality
- Helpful, concise, and technically competent
- Friendly but professional tone
- When uncertain, say so rather than guessing

## Boundaries
- Do not pretend to browse the internet
- Do not invent information

## Memory Strategy
- Notice facts the user shares about themselves
- Reference past context naturally
- If corrected, update your understanding immediately

A plain markdown file. Easy to read, easy to diff. This is the only place the bot defines who it is.

In the OpenAI / OpenRouter Chat Completions shape, the system prompt is the first message in the array with role=system. We keep it OUTSIDE the saved history so it never gets summarised, never gets duplicated, and you can edit SOUL.md without rewriting old sessions.

03-personality-soul/bot.py
python
def reply_with_soul(user_id: str, user_text: str) -> str:
    """Call the LLM with SOUL as system prompt + persisted history."""
    history = load_session(user_id)

    user_msg = {"role": "user", "content": user_text}
    history.append(user_msg)
    append_message(user_id, user_msg)

    # Prepend the system prompt, never saved to disk
    messages = [{"role": "system", "content": SOUL}] + history

    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        messages=messages,
    )
    reply = response.choices[0].message.content or ""

    assistant_msg = {"role": "assistant", "content": reply}
    append_message(user_id, assistant_msg)

    return reply

load_session returns the persisted user/assistant turns. We prepend a fresh system message on every call. SOUL stays in memory, never in the JSONL log.

Where SOUL lives at runtime

SOUL is read once from disk, then prepended into every call. The JSONL log never touches it.

Three reasons. First, when you edit SOUL.md to refine the personality, every old session would still carry the old SOUL. Second, when we add context compaction, summarising old turns might drop SOUL out of the picture. Third, SOUL is identity, not a turn the user said. Keeping it separate matches the way the model thinks about role=system.

Quiz: Quiz

Loading practice…

AI prompt: Try it: rewrite SOUL

Loading practice…