Build a Personal Decision Twin
Build an AI twin that cites your profile to guide personal decisions.
Introduction
30 Second Summary
Persondrgrsgtgsdrgsrefbgal advice can sound convincing even when it has no connection to the priorities that guide your choices. That gap matters when a recommendation affects your work, time, or money.
In this project, you will build a private browser-based Personal Decision Twin grounded in your written values, psdgergseargaereferences, and past decisions. It uses retrieval-augmented generation through OpenAI File Search and the OpenAI Responses API to recommend a choice, label its confidence, and cite the profile evidence behind it. gdhr5thrtshwrstght
What You'll Build
You'll ask your twin a real decision question in your browser and receive advice that shows how closely it reflects your documented judgment.
By the end of this project, you'll have:
- A browser-based Personal Decision Twin that answers decision questions using your documented priorities.
- Visible confidence labels and source citations that show whether about_me.md supported each recommendation.
- A repeatable calibration workflow for adding a missing decision rule. You can rebuild the knowledge base and watch the twin's advice change.
- Secret Mission: Run a blind comparison between your own answer and the twin's answer to uncover subtle differences in voice or judgment.
Are there any prerequisites?
This project assumes comfortable Python experience on an Apple silicon Mac with Python 3.10 or later plus Visual Studio Code.
You'll need an OpenAI API project with an API key plus billing or credits available.
Before We Start
A useful Personal Decision Twin needs a clear area where your priorities matter. Without that target, generic advice can sound polished while missing the judgment you want it to reflect.
This step sets a standard for the project. You will choose one decision domain where matching your real judgment would make the twin useful.
In this step, get ready to:
- Choose one recurring decision domain where your priorities shape the answer.
- Write down why matching your judgment in that domain would be useful.
Choose a meaningful decision domain
A decision domain is a recurring category of choices. Examples include career moves, weekly planning, spending decisions, or difficult conversations.
What makes a useful domain?
- Choose an area where you already have recognizable preferences.
- Choose an area where generic advice often overlooks your priorities.
- Choose an area where you can recognize whether a recommendation fits you.
- Think of a recurring category of decisions where you already have real preferences.
- Choose one domain where generic advice often misses those preferences.
- Write the domain in your usual notes app or on paper.
Capture why judgment fit matters
Your reason connects the chosen domain to a practical benefit. It defines what the twin needs to understand before its guidance can feel trustworthy.
A strong reason identifies what better-matched advice would protect or improve. This could be your time, independence, stability, growth, or relationships.
- Write one sentence explaining why matching your judgment in this domain would be useful.
- Name one personal priority that generic advice might overlook.
Before you check your note, do you expect it to describe a recurring decision area that depends on your priorities?
- Reread your chosen decision domain.
- Check that your reason explains why advice should reflect your judgment.
You should see one named decision domain plus a concrete reason that matching your judgment there would be useful.
Your twin now has a clear standard to grow toward. Next, you will set up an isolated environment for the project.
Set Up the Project
Your Personal Decision Twin depends on specific Python packages. A virtual environment gives this project its own package space.
The app also needs an OpenAI API key before it can make requests. Exporting the key in your terminal keeps the credential out of your source files.
In this step, get ready to:
- Create the project folder in Visual Studio Code.
- Build an isolated environment with the pinned dependencies.
- Export your API key and verify Streamlit in a browser.
Create the project workspace
A folder opened in Visual Studio Code becomes your workspace. Keeping every project file inside one named folder makes the later build and cleanup steps predictable.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Visual Studio Code.
- Press Enter.
You'll see the Visual Studio Code welcome screen.
- Select File from the menu bar.
- Select Open Folder...
- Select Desktop in the folder dialog.
- Select New Folder.
- Type personal-decision-twin as the folder name.
- Press Return.
- Select Open.
You'll see personal-decision-twin at the top of the Explorer sidebar.
- Confirm that you trust personal-decision-twin if a workspace trust prompt appears.
- Select View from the menu bar.
- Select Terminal.
Visual Studio Code starts its integrated terminal inside the folder shown in the Explorer. Commands you run here apply to personal-decision-twin.
- Create the project environment by running this command:
python -m venv .venv
What does this command do?
Python creates a .venv folder containing an isolated interpreter and package location. Packages installed there stay separate from your other Python projects.
- Confirm that .venv appears in the Explorer sidebar.
- Activate the environment by running this command:
source .venv/bin/activate
What does activation change?
Activation points the current terminal at the Python interpreter inside .venv. The packages you install next belong to this project.
You'll see (.venv) at the beginning of the terminal prompt.
Environment missing or inactive?
Check that the Visual Studio Code Explorer shows personal-decision-twin as the open folder. Confirm that the first command created .venv before running the activation command.
Help me diagnose why my macOS virtual environment was not created or activated inside personal-decision-twin.
Pin and install the dependencies
A pinned dependency file records the exact package versions used by the project. This prevents a future package update from changing the app unexpectedly.
- Select the new-file control beside personal-decision-twin in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Press Return.
The new requirements.txt file opens in the editor.
- Set the package versions by pasting this into requirements.txt:
openai==3.14.1
streamlit==1.64.0
What do these pins control?
- The openai package at version 3.14.1 provides the Python client used for API requests.
- The streamlit package at version 1.64.0 provides the browser interface used by the twin.
- Press Cmd+S on macOS or Ctrl+S on Windows to save requirements.txt.
- Confirm that the editor shows the two pinned package lines.
- Install the pinned dependencies by running this command:
python -m pip install -r requirements.txt
What does this command do?
Python asks pip to read requirements.txt. Pip downloads each pinned package into the active .venv environment.
You'll see package download and installation progress. The terminal returns to an active prompt when the installation finishes.
Dependency installation failed?
Confirm that (.venv) still appears at the beginning of the prompt. Check that requirements.txt contains both package lines exactly.
Help me fix the dependency installation in my activated virtual environment.
✔️ Awesome, I've got everything!
Your dependency file now pins both packages used by this project.
ⓧ I'd like to double check the full code
openai==3.14.1
streamlit==1.64.0
Export the API key and verify Streamlit
Credentials can feel risky to handle. This export keeps your API key in the current terminal session while your source files remain free of the secret.
- Return to the activated terminal from earlier.
- Replace your_api_key_here in the command below with your real OpenAI API key.
- Export the credential into the current terminal environment by running the edited command:
export OPENAI_API_KEY="your_api_key_here"
How does the app find the key?
The OpenAI SDK reads OPENAI_API_KEY from the system environment. The exported value lasts for this terminal session.
Keeping the key out of project files reduces the chance of saving or sharing it with your code.
Before you launch the example, what do you expect a successful Streamlit installation to open?
- Launch the Streamlit example by running this command:
streamlit hello
What does this command prove?
The command starts Streamlit's example app using the package installed inside .venv. A successful launch opens the example in a new browser tab.
- Confirm that your browser tab shows the Streamlit example app.
You have the setup proof now: Streamlit can serve a browser app from the activated environment.
Browser tab did not open?
Check whether the terminal still shows a running Streamlit process. If it does, copy the local address displayed there into your browser.
Help me diagnose why the Streamlit example did not open in my browser.
- Return to the terminal after capturing your screenshot.
- Press Ctrl+C to stop the Streamlit example.
Setup checkpoint
Visual Studio Code is open to personal-decision-twin. The current terminal has .venv activated.
The requirements.txt file contains the two exact version pins. Both packages are installed inside the environment.
The OPENAI_API_KEY credential exists only in the current terminal environment. The browser verification confirms that Streamlit works.
No OpenAI file or vector store exists yet.
Your isolated environment is ready. Next, you'll use it to reveal how generic an assistant sounds without evidence about your real preferences.
Experience the Generic Assistant
A confident answer can feel personal even when the model has no evidence about you. This baseline exposes that gap before you add any personal knowledge.
In this step, you will build a small Streamlit chat app. It will send personal decision questions to the OpenAI Responses API without providing facts about your preferences.
In this step, get ready to:
- Create a browser-based chat interface.
- Store the visible conversation in session state.
- Test whether the assistant can support a personal recommendation.
Create the runnable chat shell
The first slice gives the app a model setting plus a browser title. Session state prepares a place for the conversation to survive each Streamlit rerun.
- Use the file sidebar in Visual Studio Code to create app.py inside personal-decision-twin.
- Add the first runnable slice by pasting this code:
import streamlit as st
from openai import OpenAI
MODEL = "gpt-5.6-luna"
st.write("# Personal Decision Twin")
client = OpenAI()
if "messages" not in st.session_state:
st.session_state.messages = []
What Does This Code Do?
- The imports provide the browser interface plus the API client.
- The MODEL value selects gpt-5.6-luna for every request.
- The OpenAI() client reads OPENAI_API_KEY from the terminal environment.
- The st.session_state.messages list gives the conversation a persistent home during browser reruns.
- Save app.py.
- Return to the macOS terminal from the previous step.
- Stop the Streamlit example process by pressing Control+C if it is still running.
- Start your temporary app by running this command:
streamlit run app.py
What Does This Command Do?
This command starts the Streamlit development server with app.py as its entry point. It also opens the app in a browser tab.
- Switch to the browser tab opened by Streamlit.
You should see the Personal Decision Twin heading. That visible title confirms the first slice is running.
Browser Page Missing?
- Select the local address printed in the terminal if the browser did not open automatically.
- Confirm that the terminal remains inside the personal-decision-twin folder.
Ask for targeted help here: Why is my Streamlit app running without opening its browser page?
Add chat history and an input
Streamlit reruns the script when the app receives an interaction. The stored message list lets each rerun reconstruct the conversation on screen.
- In app.py, place your cursor below st.session_state.messages = [].
- Add the conversation renderer plus the chat input by pasting this code:
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
prompt = st.chat_input("Ask what choice best fits your documented preferences")
How Does the Chat Stay Visible?
- The loop renders every stored message inside the matching user or assistant chat container.
- The st.chat_input() field captures the next question submitted through the browser.
- The prompt value stays empty until you submit a question.
- Save app.py.
- Refresh the Streamlit browser tab.
You should see a chat field labelled Ask what choice best fits your documented preferences below the page heading.
Chat Field Missing?
- Confirm that the new code sits outside the session-state initialization block.
- Check the terminal for a Python syntax problem after saving app.py.
Ask for targeted help here: Why is my Streamlit chat input missing after I added the message loop?
Test the assistant with a personal question
The final slice sends each submitted question to the selected model. It displays the returned text before saving both sides of the exchange.
- In app.py, place your cursor below the st.chat_input() line.
- Add the request flow by pasting this code:
if prompt:
st.session_state.messages.append(
{"role": "user", "content": prompt, "sources": []}
)
with st.chat_message("user"):
st.write(prompt)
response = client.responses.create(
model=MODEL,
input=prompt,
max_output_tokens=500,
store=False,
)
answer = response.output_text
with st.chat_message("assistant"):
st.write(answer)
st.session_state.messages.append(
{"role": "assistant", "content": answer, "sources": []}
)
How Does the Response Flow Work?
- The first append stores your question before rendering it as a user message.
- The client.responses.create() call sends only your question to the model.
- The store=False setting disables stored Response application state for this request.
- The response.output_text property collects the returned text for display.
- The final append keeps the assistant answer in the visible conversation history.
- Save app.py.
- Refresh the Streamlit browser tab.
✔️ Awesome, I've got everything!
Your temporary app now has a model client plus a visible chat history. It can answer questions without accessing a personal profile.
ⓧ I'd like to double check the full code
import streamlit as st
from openai import OpenAI
MODEL = "gpt-5.6-luna"
st.write("# Personal Decision Twin")
client = OpenAI()
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
prompt = st.chat_input("Ask what choice best fits your documented preferences")
if prompt:
st.session_state.messages.append(
{"role": "user", "content": prompt, "sources": []}
)
with st.chat_message("user"):
st.write(prompt)
response = client.responses.create(
model=MODEL,
input=prompt,
max_output_tokens=500,
store=False,
)
answer = response.output_text
with st.chat_message("assistant"):
st.write(answer)
st.session_state.messages.append(
{"role": "assistant", "content": answer, "sources": []}
)
Before you submit the test question, do you think the assistant can cite one real preference when the app has never received your profile?
- Enter What kind of work environment would suit me? in the chat field.
- Submit the question.
You will see a polished recommendation in the assistant message. Its exact wording can vary between runs.
Look for a confident claim about your preferred environment. The app supplied no personal evidence that could support that claim.
- Choose one confident claim from the answer.
- Record why the claim lacks evidence about your actual preferences.
No Answer in the Chat?
- Confirm that the app is running in the terminal where you exported OPENAI_API_KEY.
- Check that your OpenAI API project has billing or credits available.
- Review the terminal for the request failure details.
Ask for targeted help here: Why did my temporary chat app fail to return an OpenAI response?
You now have a visible baseline for ungrounded personal advice. Next, you will build the personal knowledge source that gives the twin evidence to search.
Build Your Personal Knowledge Base
Your temporary Streamlit app exposed the gap. It can sound personal without knowing a single real preference.
A retrieval-augmented generation workflow closes that gap by searching your written profile for relevant evidence. OpenAI File Search stores that profile in a searchable vector store that your app can use in the next step.
In this step, get ready to:
- Write a personal profile with real decision rules and non-sensitive examples.
- Create scripts that build and remove the remote knowledge base.
- Upload the profile and confirm that its vector store finishes processing.
Create your private profile
Uploading personal material can feel exposed. You control the boundary by including only the details needed to explain how you make decisions.
Set a privacy boundary
Keep passwords and authentication secrets out of this profile. Exclude financial account details too.
Leave out medical records and government identifiers. Use concise examples that you are comfortable sending to the OpenAI API.
- Switch back to Visual Studio Code from earlier.
- Return to the file sidebar for the personal-decision-twin folder.
- Use the folder creation control to create a folder named data.
- Select the data folder.
- Use the file creation control to create about_me.md inside data.
- Add the first part of the profile template to data/about_me.md by pasting this code:
# Personal Profile
Only include information you are comfortable sending to the OpenAI API. Do not include passwords, authentication secrets, financial account details, medical records, or government identifiers.
## Core values
- Write three values that regularly shape your decisions.
- For each value, write what it looks like in practice.
## Decision priorities
- Write what you usually optimize for when choosing between speed, quality, cost, freedom, stability, and growth.
- Write one condition that causes you to change those priorities.
## Work style
- Describe the environments in which you do your best work.
- Describe how you prefer to plan, collaborate, and receive feedback.
## Communication style
- Describe your normal tone, preferred answer length, and how direct you want advice to be.
- Include two short examples of how you would explain a difficult choice.
## Boundaries
- List choices you would normally reject, even if they offer a short-term benefit.
- List topics where the twin should say it lacks enough information.
How does this structure help?
- The values section captures the principles behind your choices.
- The priorities section records how you handle trade-offs.
- The work and communication sections describe the conditions that fit you.
- The boundaries section tells the twin when it should avoid a recommendation.
- Save data/about_me.md.
You should see about_me.md listed inside the data folder. The editor should show the profile title plus five section headings.
Profile file in the wrong place?
Confirm that about_me.md appears inside data. Move the file there if it appears beside app.py.
Help me check the location of my profile file.
- Add the past-decision section below the final boundary instruction by pasting this code:
## Past decisions
### Decision one
Situation: Write a real situation here.
Choice: Write what you chose.
Reasoning: Write the values and trade-offs behind the choice.
### Decision two
Situation: Write a different real situation here.
Choice: Write what you chose.
Reasoning: Write the values and trade-offs behind the choice.
### Decision three
Situation: Write a third real situation here.
Choice: Write what you chose.
Reasoning: Write the values and trade-offs behind the choice.
Why include past decisions?
Past decisions connect your stated values to choices you actually made. Each situation gives File Search concrete evidence for similar questions.
- Save data/about_me.md.
You should now see three decision headings under Past decisions.
- Rewrite the Core values instructions with three values plus one practical example for each.
- Rewrite the Decision priorities instructions with your usual trade-offs plus one condition that changes them.
- Rewrite the Work style instructions with the environments and collaboration habits that suit you.
- Rewrite the Communication style instructions with your preferred tone plus two short examples.
- Rewrite the Boundaries instructions with choices you reject plus topics where evidence may be insufficient.
- Complete all three past decisions with a real situation plus your choice plus your reasoning.
- Save data/about_me.md.
Every template instruction should now be replaced with your own concise examples. The file should contain no credentials or highly sensitive records.
Unsure what to write?
Use ordinary choices with clear trade-offs. A job offer or a purchase or a difficult conversation can reveal a useful decision rule without exposing sensitive data.
Help me turn a past choice into a safe profile example.
Create the build and cleanup scripts
The build script manages the full knowledge-base lifecycle. It removes resources recorded by an earlier run before uploading the latest profile.
- Select the personal-decision-twin folder in the file sidebar.
- Use the file creation control to create build_knowledge.py beside app.py.
- Add the imports plus the resource paths plus the previous-resource cleanup function by pasting this code:
import json
from pathlib import Path
from openai import OpenAI
PROFILE_PATH = Path("data/about_me.md")
RESOURCE_PATH = Path("resource_ids.json")
def delete_previous_resources(client: OpenAI) -> None:
if not RESOURCE_PATH.exists():
return
resources = json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
file_id = resources.get("file_id")
vector_store_id = resources.get("vector_store_id")
if file_id:
try:
client.files.delete(file_id)
except Exception as error:
print(f"Could not delete the previous file: {error}")
if vector_store_id:
try:
client.vector_stores.delete(vector_store_id)
except Exception as error:
print(f"Could not delete the previous vector store: {error}")
RESOURCE_PATH.unlink(missing_ok=True)
What does this code do?
- The paths identify your completed profile and the local record of remote resource IDs.
- The cleanup function reads any previously saved IDs before deleting those remote resources.
- The final unlink removes the outdated local ID record.
- Save build_knowledge.py.
You should see build_knowledge.py beside app.py. The file should end with RESOURCE_PATH.unlink(missing_ok=True).
Seeing syntax markers?
Check the indentation inside each try block. Confirm that each matching except line aligns with its try line.
Help me check the cleanup function for indentation errors.
- Add the first part of main() below RESOURCE_PATH.unlink(missing_ok=True) by pasting this code:
def main() -> None:
if not PROFILE_PATH.exists():
raise SystemExit("Create data/about_me.md before building the knowledge base.")
client = OpenAI()
delete_previous_resources(client)
uploaded_file = client.files.create(
file=PROFILE_PATH,
purpose="assistants",
)
vector_store = client.vector_stores.create(name="Personal Decision Twin")
try:
indexed_file = client.vector_stores.files.create_and_poll(
file_id=uploaded_file.id,
vector_store_id=vector_store.id,
max_wait_seconds=60,
)
if indexed_file.status != "completed":
raise RuntimeError(f"Indexing ended with status: {indexed_file.status}")
except Exception:
client.files.delete(uploaded_file.id)
client.vector_stores.delete(vector_store.id)
raise
How does the upload work?
- The profile check stops the build when data/about_me.md is missing.
- The OpenAI client reads OPENAI_API_KEY from your terminal environment.
- The upload becomes the source file for a vector store named Personal Decision Twin.
- The polling call waits for processing and removes both new resources if indexing fails.
- Save build_knowledge.py.
You should see def main() -> None: below the cleanup function. Its final line should currently be raise.
Main function outside the file?
Confirm that def main() -> None: begins at the far left. Keep every line inside the function indented by four spaces.
Help me check where the main function belongs.
- Add the resource-saving output below the final raise by pasting this code:
resources = {
"file_id": uploaded_file.id,
"vector_store_id": vector_store.id,
}
RESOURCE_PATH.write_text(
json.dumps(resources, indent=2),
encoding="utf-8",
)
print("Knowledge base ready.")
print(f"Status: {indexed_file.status}")
print(f"Vector store ID: {vector_store.id}")
print(f"File ID: {uploaded_file.id}")
if __name__ == "__main__":
main()
Why save the resource IDs?
The script writes both generated IDs to resource_ids.json. Your app and cleanup script can now target the same uploaded file and vector store.
The final guard runs main() when you execute the file from the terminal.
- Save build_knowledge.py.
The file should now end with main(). The four output lines should appear directly above the final guard.
Resource lines aligned incorrectly?
Keep the resources dictionary plus the write call plus the print calls inside main(). Keep the final guard at the far left.
Help me check the final indentation in the build script.
✔️ Awesome, I've got everything!
Great. Confirm that you saved build_knowledge.py before continuing.
ⓧ I'd like to double check the full code
import json
from pathlib import Path
from openai import OpenAI
PROFILE_PATH = Path("data/about_me.md")
RESOURCE_PATH = Path("resource_ids.json")
def delete_previous_resources(client: OpenAI) -> None:
if not RESOURCE_PATH.exists():
return
resources = json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
file_id = resources.get("file_id")
vector_store_id = resources.get("vector_store_id")
if file_id:
try:
client.files.delete(file_id)
except Exception as error:
print(f"Could not delete the previous file: {error}")
if vector_store_id:
try:
client.vector_stores.delete(vector_store_id)
except Exception as error:
print(f"Could not delete the previous vector store: {error}")
RESOURCE_PATH.unlink(missing_ok=True)
def main() -> None:
if not PROFILE_PATH.exists():
raise SystemExit("Create data/about_me.md before building the knowledge base.")
client = OpenAI()
delete_previous_resources(client)
uploaded_file = client.files.create(
file=PROFILE_PATH,
purpose="assistants",
)
vector_store = client.vector_stores.create(name="Personal Decision Twin")
try:
indexed_file = client.vector_stores.files.create_and_poll(
file_id=uploaded_file.id,
vector_store_id=vector_store.id,
max_wait_seconds=60,
)
if indexed_file.status != "completed":
raise RuntimeError(f"Indexing ended with status: {indexed_file.status}")
except Exception:
client.files.delete(uploaded_file.id)
client.vector_stores.delete(vector_store.id)
raise
resources = {
"file_id": uploaded_file.id,
"vector_store_id": vector_store.id,
}
RESOURCE_PATH.write_text(
json.dumps(resources, indent=2),
encoding="utf-8",
)
print("Knowledge base ready.")
print(f"Status: {indexed_file.status}")
print(f"Vector store ID: {vector_store.id}")
print(f"File ID: {uploaded_file.id}")
if __name__ == "__main__":
main()
Compare this reference with your file from top to bottom. The build script should match it exactly.
The uploaded file and vector store persist until deletion. A dedicated cleanup script gives you a repeatable way to remove both resources.
- Select the personal-decision-twin folder in the file sidebar.
- Use the file creation control to create cleanup.py beside build_knowledge.py.
- Add the remote-resource cleanup workflow by pasting this code:
import json
from pathlib import Path
from openai import OpenAI
RESOURCE_PATH = Path("resource_ids.json")
def main() -> None:
if not RESOURCE_PATH.exists():
print("No saved remote resources were found.")
return
resources = json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
client = OpenAI()
file_id = resources.get("file_id")
vector_store_id = resources.get("vector_store_id")
if file_id:
client.files.delete(file_id)
if vector_store_id:
client.vector_stores.delete(vector_store_id)
RESOURCE_PATH.unlink()
print("Remote resources deleted.")
if __name__ == "__main__":
main()
What does the cleanup script remove?
- The script reads the saved file ID plus vector store ID from resource_ids.json.
- It deletes the uploaded file when a file ID exists.
- It deletes the vector store when a vector store ID exists.
- It removes the local ID record after both remote deletion calls complete.
- Save cleanup.py.
You should see cleanup.py beside build_knowledge.py in the file sidebar.
Cleanup file missing?
Confirm that the filename is exactly cleanup.py. Check that it appears in personal-decision-twin beside the other Python files.
Help me check my cleanup script.
✔️ Awesome, I've got everything!
Your cleanup path is ready. Keep cleanup.py saved for the end of the project.
ⓧ I'd like to double check the full code
import json
from pathlib import Path
from openai import OpenAI
RESOURCE_PATH = Path("resource_ids.json")
def main() -> None:
if not RESOURCE_PATH.exists():
print("No saved remote resources were found.")
return
resources = json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
client = OpenAI()
file_id = resources.get("file_id")
vector_store_id = resources.get("vector_store_id")
if file_id:
client.files.delete(file_id)
if vector_store_id:
client.vector_stores.delete(vector_store_id)
RESOURCE_PATH.unlink()
print("Remote resources deleted.")
if __name__ == "__main__":
main()
Compare this reference with your cleanup file. Every deletion depends on the IDs saved by the build script.
Build and verify the knowledge base
This build uploads your profile to persistent remote storage. It also creates a processed vector store that remains available until you delete it.
Know what can be billed
The first 1 GB of File Search storage is free. Storage above that allowance is billed daily.
File Search calls plus model tokens are also billable when you use the app. Your cleanup.py script removes both persistent resources when you finish.
Before you run the build, do you expect the profile to finish processing or stop with an error?
- Switch back to the terminal that has your active .venv plus exported OPENAI_API_KEY.
- Press Ctrl+C to stop the running Streamlit process.
- Build the personal knowledge base by running this command:
python build_knowledge.py
What does this run create?
The script uploads data/about_me.md with purpose="assistants". It creates the Personal Decision Twin vector store.
Processing can take up to 60 seconds. The terminal should finish with Status: completed plus a vector store ID plus a file ID.
That is the retrieval layer working. Your profile is now uploaded and searchable for the grounded app you build next.
Build did not complete?
A missing-profile message means data/about_me.md is in the wrong location. An authentication failure means the current terminal cannot read your exported API key.
A processing failure can leave no valid ID record. Fix the reported issue before running the build again.
Help me diagnose my knowledge-base build error.
- Return to VS Code after the terminal prints the completed status.
- Select resource_ids.json in the file sidebar.
- Confirm that the JSON object contains file_id plus vector_store_id.
The values in resource_ids.json should match the current IDs printed in your terminal. This proves that later scripts can target the resources you just created.
Your personal evidence now has a searchable home. Next up, you will connect the temporary chat app to this knowledge base and make every grounded answer show its evidence.
Ground the Twin With Evidence
Your personal profile is now indexed in a vector store. The current app still answers without searching that evidence.
This step connects your Streamlit app to OpenAI File Search. The finished flow reveals whether personal evidence supports each answer.
In this step, get ready to:
- Replace the generic request with a profile-grounded response.
- Require structured guidance with a confidence label.
- Expose each answer's evidence status below the response.
Define the twin's evidence rules
Retrieval-augmented generation gives the model relevant knowledge at response time. Prompt instructions control how the model uses that knowledge.
- Switch back to app.py in the VS Code editor.
- Select the temporary generic code.
- Replace it with this grounded foundation:
import json
from pathlib import Path
import streamlit as st
from openai import OpenAI
MODEL = "gpt-5.6-luna"
RESOURCE_PATH = Path("resource_ids.json")
INSTRUCTIONS = """
You are Personal Decision Twin, a transparent AI simulation of the user's documented preferences. You do not literally think like the user and must never claim to be the user.
Always search the personal profile before answering. Treat that profile as the only source of truth about the user's values, preferences, history, boundaries, and communication style. Do not invent personal facts.
Return four short sections with these exact headings:
Recommendation
Why this fits the profile
Confidence
Missing information
Set Confidence to High only when the retrieved profile directly supports the recommendation, Medium when it supports only part of the reasoning, and Low when important evidence is missing. When confidence is Low, ask one focused follow-up question instead of pretending to know the user's view.
"""
st.write("# Personal Decision Twin")
st.write(
"This is an AI simulation grounded in your written profile. "
"It does not literally think like you and should not impersonate you."
)
What does this foundation do?
- The instructions make your profile the only source of personal facts.
- The required headings make confidence gaps visible.
- The simulation statement prevents the app from claiming your identity.
- The low-confidence rule turns missing evidence into a focused follow-up question.
- Save app.py.
- Return to the terminal from earlier.
- Start the updated app by running this command:
streamlit run app.py
What should you see?
Your browser shows the Personal Decision Twin title. It also states that the app is an AI simulation grounded in your written profile.
🙋♀️ App does not start?
Confirm that the terminal prompt shows your active .venv environment. Confirm that you saved app.py before starting the app.
Check the copied instruction string for missing triple quotes.
Help me diagnose why my grounded Streamlit app does not start.
Load the profile and chat shell
The local resource_ids.json file connects the app to your processed profile. Citation extraction converts response annotations into filenames that the interface can display.
- Place your cursor above the first st.write() call in app.py.
- Add the resource-loading and citation-extraction helpers below the instruction string:
def load_resources() -> dict[str, str] | None:
if not RESOURCE_PATH.exists():
return None
return json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
def extract_citations(response: object) -> list[str]:
response_data = response.to_dict()
filenames = set()
for item in response_data.get("output", []):
if item.get("type") != "message":
continue
for content in item.get("content", []):
if content.get("type") != "output_text":
continue
for annotation in content.get("annotations", []):
if annotation.get("type") == "file_citation":
filename = annotation.get("filename")
if filename:
filenames.add(filename)
return sorted(filenames)
How do the helpers work?
- The resource loader reads the saved vector store ID from resource_ids.json.
- The citation extractor converts the response into a dictionary.
- The nested loops inspect output text annotations for cited files.
- The set removes duplicate filenames before the sorted list reaches the interface.
- Find the existing page title block below the helper functions.
- Replace that block with this resource-aware chat shell:
st.write("# Personal Decision Twin")
st.write(
"This is an AI simulation grounded in your written profile. "
"It does not literally think like you and should not impersonate you."
)
resources = load_resources()
if resources is None:
st.write("Knowledge base not ready. Run python build_knowledge.py first.")
else:
client = OpenAI()
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
if message.get("sources"):
st.write("Sources: " + ", ".join(message["sources"]))
prompt = st.chat_input("Ask what choice best fits your documented preferences")
What does the chat shell do?
- The resource check prevents a request when the saved knowledge base is unavailable.
- The session state keeps previous user and assistant messages visible.
- The message renderer restores source filenames alongside earlier answers.
- The chat input collects the next personal decision question.
- Save app.py.
- Refresh the browser tab running your app.
You'll see the simulation notice above a chat input for questions about your documented preferences.
Chat input is missing?
Check that resource_ids.json remains inside the personal-decision-twin folder. The app shows a knowledge-base warning when that file is missing.
Check that the chat shell remains indented beneath the else: branch.
Help me find why the chat input is missing.
Retrieve answers and show sources
The request now needs to search the saved profile for every question. The response renderer then separates cited guidance from unsupported inference.
- Place your cursor below the st.chat_input() line.
- Add the grounded request and initial response renderer:
if prompt:
st.session_state.messages.append(
{"role": "user", "content": prompt, "sources": []}
)
with st.chat_message("user"):
st.write(prompt)
response = client.responses.create(
model=MODEL,
instructions=INSTRUCTIONS,
input=prompt,
tools=[
{
"type": "file_search",
"vector_store_ids": [resources["vector_store_id"]],
"max_num_results": 4,
}
],
max_output_tokens=500,
store=False,
)
answer = response.output_text
citations = extract_citations(response)
with st.chat_message("assistant"):
st.write(answer)
How does grounding happen?
- The request passes your question to the selected model.
- The File Search tool searches the vector store referenced by resource_ids.json.
- The result limit keeps retrieval focused on four profile matches.
- The response text becomes the visible assistant answer.
- Place your cursor directly below st.write(answer).
- Complete the assistant response with this evidence display and history update:
if citations:
st.write("Sources: " + ", ".join(citations))
else:
st.write(
"Sources: no profile citation returned. "
"Treat this answer as a low-confidence inference."
)
st.session_state.messages.append(
{"role": "assistant", "content": answer, "sources": citations}
)
Why show the source status?
A cited filename proves that the response used retrieved profile evidence. The fallback warning exposes answers that lack that support.
Saving the citations with each assistant message keeps that evidence visible throughout the chat history.
✔️ Awesome, I've got everything!
Great. Save app.py before testing the complete grounded flow.
ⓧ I'd like to double check the full code
import json
from pathlib import Path
import streamlit as st
from openai import OpenAI
MODEL = "gpt-5.6-luna"
RESOURCE_PATH = Path("resource_ids.json")
INSTRUCTIONS = """
You are Personal Decision Twin, a transparent AI simulation of the user's documented preferences. You do not literally think like the user and must never claim to be the user.
Always search the personal profile before answering. Treat that profile as the only source of truth about the user's values, preferences, history, boundaries, and communication style. Do not invent personal facts.
Return four short sections with these exact headings:
Recommendation
Why this fits the profile
Confidence
Missing information
Set Confidence to High only when the retrieved profile directly supports the recommendation, Medium when it supports only part of the reasoning, and Low when important evidence is missing. When confidence is Low, ask one focused follow-up question instead of pretending to know the user's view.
"""
def load_resources() -> dict[str, str] | None:
if not RESOURCE_PATH.exists():
return None
return json.loads(RESOURCE_PATH.read_text(encoding="utf-8"))
def extract_citations(response: object) -> list[str]:
response_data = response.to_dict()
filenames = set()
for item in response_data.get("output", []):
if item.get("type") != "message":
continue
for content in item.get("content", []):
if content.get("type") != "output_text":
continue
for annotation in content.get("annotations", []):
if annotation.get("type") == "file_citation":
filename = annotation.get("filename")
if filename:
filenames.add(filename)
return sorted(filenames)
st.write("# Personal Decision Twin")
st.write(
"This is an AI simulation grounded in your written profile. "
"It does not literally think like you and should not impersonate you."
)
resources = load_resources()
if resources is None:
st.write("Knowledge base not ready. Run python build_knowledge.py first.")
else:
client = OpenAI()
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
if message.get("sources"):
st.write("Sources: " + ", ".join(message["sources"]))
prompt = st.chat_input("Ask what choice best fits your documented preferences")
if prompt:
st.session_state.messages.append(
{"role": "user", "content": prompt, "sources": []}
)
with st.chat_message("user"):
st.write(prompt)
response = client.responses.create(
model=MODEL,
instructions=INSTRUCTIONS,
input=prompt,
tools=[
{
"type": "file_search",
"vector_store_ids": [resources["vector_store_id"]],
"max_num_results": 4,
}
],
max_output_tokens=500,
store=False,
)
answer = response.output_text
citations = extract_citations(response)
with st.chat_message("assistant"):
st.write(answer)
if citations:
st.write("Sources: " + ", ".join(citations))
else:
st.write(
"Sources: no profile citation returned. "
"Treat this answer as a low-confidence inference."
)
st.session_state.messages.append(
{"role": "assistant", "content": answer, "sources": citations}
)
- Save app.py.
- Refresh the browser tab running your app.
Before you ask, will the twin cite your profile or fall back to a low-confidence inference?
- Enter a decision question that your profile directly covers.
- Submit the question.
You'll see Recommendation, Why this fits the profile, Confidence, and Missing information in the answer.
Below the answer, you'll see Sources: about_me.md.
That's the key shift complete. Your twin now shows the evidence behind its personal guidance.
Source filename is missing?
Ask about a preference that appears explicitly in data/about_me.md.
Confirm that resource_ids.json still contains the IDs from your completed knowledge build.
Help me diagnose why File Search returned no profile citation.
Your twin can now retrieve your profile and expose its evidence. Next, you'll test whether that evidence produces advice that matches your real judgment.
Calibrate Against Real Decisions
Your Personal Decision Twin now uses File Search to ground its guidance in your profile. Its source line shows when about_me.md supports an answer.
Calibration compares that guidance with decisions whose answers you already know. A mismatch exposes a rule or example your profile has not captured.
In this step, get ready to:
- Test the twin with three known decisions.
- Add one missing decision rule plus one concrete example to data/about_me.md.
- Rebuild the knowledge base to verify the revised answer.
Test three known decisions
Known-answer questions give you a baseline for judging the twin. Each result earns one simple rating.
How should I rate each answer?
Use these labels consistently across all three questions.
- Match means the recommendation reflects your known choice. The reasoning follows the priorities you actually use.
- Partial match means the direction feels right. One important trade-off is missing or misweighted.
- Mismatch means the advice conflicts with your real judgment. The reasoning relies on the wrong priority.
- Write one career-choice question whose answer you already know.
- Write one time-or-money trade-off question whose answer you already know.
- Write one communication-decision question whose answer you already know.
- Record your own expected answer below each question.
- Return to the browser tab running your Personal Decision Twin.
Before you test the questions, which category do you expect to reveal the clearest mismatch?
- Submit the three questions one at a time through the chat input.
- Record each full response in your calibration notes.
- Label each response as match, partial match, or mismatch.
Your calibration notes should now contain three known decisions with one rating apiece.
Find a useful mismatch
If every answer feels close, use the weakest result as your calibration target. A partial match still exposes missing profile evidence.
- Choose the response with the clearest mismatch.
- Save its question here: your mismatched decision question.
Add the missing rule to your profile
The twin can retrieve only the decision rules your profile contains. A concrete example shows how the rule changes a real choice.
Write a reusable rule
A reusable decision rule names the condition that changes your choice. A strong example shows that condition affecting one real decision.
Concise non-sensitive details give the twin useful evidence. They keep the profile suitable for API retrieval.
- In the Explorer sidebar in Visual Studio Code, select data/about_me.md inside personal-decision-twin.
You should see your completed profile with its existing values, priorities, work style, communication style, boundaries, and past decisions.
- Find the profile section most closely related to the mismatch.
- Add the missing decision rule in one concise sentence.
The new rule should now be visible beside the profile evidence it clarifies.
- Add one non-sensitive real example on the next line.
- Save data/about_me.md.
That gap now has a concrete fix. Your local profile contains evidence the twin can retrieve during the next test.
Rebuild and retest the twin
Your running app loaded the current vector store ID from resource_ids.json. The rebuild replaces that index with your revised profile.
Replacing remote resources can feel risky. Your saved data/about_me.md stays on your Mac throughout the rebuild.
- Return to the Terminal panel where Streamlit is running.
- Press Ctrl+C in that terminal to stop the running app.
The rebuild may spend close to one minute processing your revised profile. A quiet terminal during this wait is expected.
- Rebuild the knowledge base by running this command:
python build_knowledge.py
What does this rebuild do?
The script deletes the current OpenAI file. It also deletes the current vector store.
It uploads the revised data/about_me.md. It writes replacement IDs to resource_ids.json after indexing completes.
- Check that the terminal prints Status: completed.
The saved IDs now point to the replacement upload. They also point to its processed vector store.
Rebuild did not complete?
- Confirm the project virtual environment is still active.
- Confirm the current terminal still has the API key exported.
- Confirm data/about_me.md was saved before you rerun the command.
Help me diagnose why my Personal Decision Twin knowledge-base rebuild did not complete.
- Restart the Personal Decision Twin by running this command:
streamlit run app.py
What does this command do?
Streamlit starts app.py again. The fresh process reads the replacement vector_store_id from resource_ids.json.
- Return to the browser tab from earlier.
The Personal Decision Twin interface should reconnect to the restarted app.
Before you repeat the question, which part of the answer do you expect the new rule to change?
- Submit your mismatched decision question again through the chat input.
- Compare the revised answer with the previous result in your calibration notes.
- Confirm the added rule shapes the recommendation or its reasoning.
- Confirm the source line displays Sources: about_me.md.
The repeated answer should now reflect the added decision rule. Its source line should show Sources: about_me.md.
Still seeing the old advice?
- Check that the rebuild finished with Status: completed.
- Confirm resource_ids.json was updated during the rebuild.
- Restart the app from the rebuilt project state.
Help me work out why my rebuilt Personal Decision Twin still gives its previous answer.
Strong work. Your twin now has a tested rule grounded in a real decision.
Secret mission
Run a Blind Twin Test
Put your Personal Decision Twin through a blind comparison using a decision question that it has never answered before. A trusted reviewer will identify which unlabeled response sounds more like you and reveal one precise gap for a future profile update.
Clean Up Your Resources
Clean Up Your Resources
Your local project stays free on your Mac. The uploaded OpenAI file plus vector store persist until you delete them.
Cost warning
File Search storage includes 1 GB at no charge. Storage above that allowance costs $0.10 per GB per day.
Future File Search calls cost $2.50 per 1,000 calls. Model requests add token charges at the selected model's rates.
The Pause option keeps the remote resources available. Delete prevents future storage charges from those resources.
Resources you used:
- The local personal-decision-twin folder with its .venv plus project files.
- The current OpenAI upload of data/about_me.md.
- The current OpenAI vector store named Personal Decision Twin.
- The Streamlit process if it is still active.
Keep everything running
No action is needed. Choose this option if you want to continue testing your twin or refine its profile.
- Leave the Streamlit process active if it is still running.
- Keep the personal-decision-twin folder for future profile edits.
- Keep resource_ids.json so the app can find the current vector store.
- Allow for additional usage charges when you ask more questions.
Pause - I'll come back to this later
Stop the local process to free its memory. Your project files plus remote knowledge base remain available for your return.
- Stop the Streamlit app by pressing Ctrl+C in the terminal where it runs.
- Exit the project virtual environment by running this command:
deactivate
What does this command do?
The command exits .venv in your current terminal. Your installed packages remain inside the project folder.
Your terminal prompt should no longer show (.venv). The OpenAI file plus vector store remain available.
Delete - I don't want to use this again
Deletion permanently removes the remote knowledge base. Your local profile remains available until the final folder-removal step.
- Switch back to the active project terminal from earlier.
- Delete the current uploaded file plus vector store by running this command:
python cleanup.py
What does cleanup.py remove?
- The script reads the current IDs from resource_ids.json.
- The script deletes the uploaded profile file through OpenAI.
- The script deletes the Personal Decision Twin vector store.
- The script removes resource_ids.json after the remote cleanup succeeds.
You should see the message Remote resources deleted. That is the persistent remote state cleared.
Cleanup script failed?
- Confirm you are using the active .venv from earlier.
- Confirm the current terminal still has OPENAI_API_KEY exported.
- Run the command from personal-decision-twin where cleanup.py exists.
Help me diagnose why cleanup.py could not delete my OpenAI file or vector store.
- Exit the project virtual environment by running this command:
deactivate
Why deactivate first?
The command exits .venv before you remove its folder. This leaves your terminal outside the project-specific Python environment.
Your terminal prompt should no longer show (.venv).
You still have one last chance to keep any notes you value. The next command permanently removes your local profile plus project code.
- Save any profile or calibration notes you want to keep outside the personal-decision-twin folder.
- Remove the local project folder by running these commands:
cd ..
rm -rf personal-decision-twin
ls
What do these commands do?
- The first command moves your terminal one level above personal-decision-twin.
- The second command permanently deletes the entire project folder.
- The final command lists the remaining contents of the parent folder for verification.
You should no longer see personal-decision-twin in the ls output.
Still see the project folder?
- Check that your terminal moved to the folder above personal-decision-twin.
- Confirm the project folder name is spelled exactly as personal-decision-twin.
Help me safely remove the remaining personal-decision-twin folder.
Nice Work!
Nice Work!
You did it! You built a browser-based Personal Decision Twin grounded in your own written profile.
You've learned how to:
- Built a browser-based Personal Decision Twin with Python plus Streamlit. It uses OpenAI File Search to retrieve your documented preferences.
- Made grounding evidence visible through about_me.md citations. You also added confidence labels that expose unsupported inference.
- Ran a three-question calibration against decisions whose answers you already knew. You improved the profile with one missing decision rule.
- Secret Mission: Completed a blind twin test with a trusted reviewer. You captured one specific difference in tone, priorities, or reasoning for the next profile update.
Ready to quiz yourself?