This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
What I Built
I built FriendOS for a friend who constantly has important things scattered across messages, notes, and random conversations.
The idea is simple: give the app something my friend said, and it turns the chaos into useful memories — tasks, dates, commitments, and things worth remembering. You can then ask questions like “When is my interview?” or “What did I say I needed to finish?”
The important part is that this isn't another AI app that sends someone's personal life to a server they don't control.
FriendOS is built around open-source AI and local inference. The model can run on the user's own machine, which means their memories can stay on their computer. There's no required API key, no per-request bill, and no dependency on a proprietary AI provider.
That's why open innovation mattered for this project.
A friend's messages and personal memories are exactly the kind of data I don't want to blindly upload somewhere. With an open model, I can choose the model, run it locally, swap it out, inspect how the system works, and eventually fine-tune or customize it for the person using it.
I deliberately kept the project small. This weekend's challenge was “Build for a Friend,” so I wanted to build something for one real person rather than another generic AI demo.
The goal wasn't to build the smartest memory system.
It was to build something my friend could actually use.
And then hand it to them.
Code
app.py
import json
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
PORT = 8000
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "qwen2.5:3b"
BASE_DIR = Path(file).parent
MEMORY_FILE = BASE_DIR / "memories.json"
HTML_FILE = BASE_DIR / "index.html"
def load_memories():
try:
return json.loads(MEMORY_FILE.read_text())
except Exception:
return []
def save_memories(memories):
MEMORY_FILE.write_text(json.dumps(memories, indent=2))
def ask_ollama(prompt):
payload = json.dumps({
"model": MODEL,
"prompt": prompt,
"stream": False
}).encode()
request = urllib.request.Request(
OLLAMA_URL,
data=payload,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(request, timeout=120) as response:
data = json.loads(response.read().decode())
return data["response"]
def extract_memories(text):
prompt = f"""
You are FriendMemory, a private personal memory assistant.
Analyze the following message from a person's friend.
Extract only information that would actually be useful to remember later.
Return ONLY valid JSON in this exact format:
[
{{
"type": "task",
"title": "short title",
"detail": "useful detail"
}}
]
Allowed types:
- task
- date
- person
- preference
- commitment
- important
Message:
{text}
"""
response = ask_ollama(prompt)
# Handle models that wrap JSON in markdown.
response = response.strip()
if response.startswith("
response = response.replace("```
json", "").replace("
", "").strip()
try:
result = json.loads(response)
if not isinstance(result, list):
return []
return result
except Exception:
return []
def answer_question(question, memories):
memory_text = "\n".join(
f"- {m.get('type', 'memory')}: {m.get('title', '')} — {m.get('detail', '')}"
for m in memories
)
prompt = f"""
You are a private memory assistant.
Answer the user's question using ONLY the memories below.
If the answer isn't present, say:
"I don't have that in my memories."
Keep the answer short and natural.
Memories:
{memory_text}
Question:
{question}
"""
return ask_ollama(prompt).strip()
class Handler(BaseHTTPRequestHandler):
def send_json(self, data, status=200):
body = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/":
body = HTML_FILE.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif self.path == "/memories":
self.send_json(load_memories())
else:
self.send_error(404)
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
data = json.loads(body.decode())
except Exception:
self.send_json({"error": "Invalid JSON"}, 400)
return
if self.path == "/remember":
text = data.get("text", "").strip()
if not text:
self.send_json({"error": "No text provided"}, 400)
return
try:
extracted = extract_memories(text)
except Exception as e:
self.send_json({
"error": f"Could not contact Ollama: {e}"
}, 500)
return
memories = load_memories()
for memory in extracted:
memory["source"] = text
memories.append(memory)
save_memories(memories)
self.send_json({
"added": extracted,
"all": memories
})
elif self.path == "/ask":
question = data.get("question", "").strip()
if not question:
self.send_json({"error": "No question provided"}, 400)
return
try:
answer = answer_question(question, load_memories())
except Exception as e:
self.send_json({
"error": f"Could not contact Ollama: {e}"
}, 500)
return
self.send_json({"answer": answer})
elif self.path == "/clear":
save_memories([])
self.send_json({"success": True})
else:
self.send_error(404)
print(f"FriendMemory running at http://localhost:{PORT}")
HTTPServer(("localhost", PORT), Handler).serve_forever()
index.html
<!DOCTYPE html>
- { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: #0b1020;
color: #f8fafc;
}
.container {
max-width: 900px;
margin: auto;
padding: 50px 20px;
}
.hero {
margin-bottom: 35px;
}
.badge {
display: inline-block;
background: #172554;
color: #93c5fd;
padding: 7px 12px;
border-radius: 999px;
font-size: 13px;
margin-bottom: 15px;
}
h1 {
font-size: 48px;
margin: 0 0 10px;
}
.subtitle {
color: #94a3b8;
font-size: 18px;
line-height: 1.6;
}
.card {
background: #111827;
border: 1px solid #1f2937;
border-radius: 18px;
padding: 22px;
margin-bottom: 20px;
}
textarea,
input {
width: 100%;
background: #020617;
color: white;
border: 1px solid #334155;
border-radius: 12px;
padding: 15px;
font-size: 15px;
outline: none;
}
textarea {
min-height: 150px;
resize: vertical;
}
textarea:focus,
input:focus {
border-color: #6366f1;
}
button {
border: 0;
border-radius: 10px;
padding: 12px 18px;
background: #6366f1;
color: white;
font-weight: 700;
cursor: pointer;
margin-top: 12px;
}
button:hover {
background: #4f46e5;
}
.secondary {
background: #1e293b;
}
.memories {
display: grid;
gap: 12px;
margin-top: 18px;
}
.memory {
background: #020617;
border: 1px solid #1e293b;
padding: 16px;
border-radius: 12px;
}
.memory-type {
color: #818cf8;
text-transform: uppercase;
font-size: 11px;
font-weight: bold;
letter-spacing: 1px;
}
.memory-title {
font-size: 17px;
font-weight: 700;
margin-top: 5px;
}
.memory-detail {
color: #94a3b8;
margin-top: 5px;
}
.answer {
margin-top: 15px;
padding: 15px;
background: #172554;
border-radius: 12px;
color: #bfdbfe;
}
.status {
color: #94a3b8;
margin-top: 10px;
min-height: 20px;
}
.privacy {
text-align: center;
color: #64748b;
margin-top: 30px;
font-size: 13px;
}
LOCAL AI · OPEN WEIGHT · PRIVATE
<h1>🧠 FriendMemory</h1>
A tiny AI memory assistant for someone you care about.
Turn messy messages into memories you can actually find later.
<h2>💬 Remember something</h2>
Remember this
<h2>🔎 Ask your memories</h2>
Ask
<h2>💭 Your memories</h2>
Clear everything
🔒 Your memories stay on your computer.
AI inference happens locally through Ollama.
async function remember() {
const text = document.getElementById("message").value;
const status = document.getElementById("status");
if (!text.trim()) return;
status.textContent = "Thinking...";
try {
const response = await fetch("/remember", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
const data = await response.json();
if (data.error) {
status.textContent = data.error;
return;
}
status.textContent =
`Added ${data.added.length} memory${data.added.length === 1 ? "" : "ies"}.`;
document.getElementById("message").value = "";
renderMemories(data.all);
} catch (error) {
status.textContent =
"Could not connect to the local AI server.";
}
}
async function ask() {
const question = document.getElementById("question").value;
const answerBox = document.getElementById("answer");
if (!question.trim()) return;
answerBox.innerHTML = "Thinking...";
try {
const response = await fetch("/ask", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ question })
});
const data = await response.json();
answerBox.innerHTML =
`<div class="answer">${escapeHtml(data.answer)}</div>`;
} catch (error) {
answerBox.innerHTML =
`<div class="answer">Something went wrong.</div>`;
}
}
async function loadMemories() {
const response = await fetch("/memories");
const memories = await response.json();
renderMemories(memories);
}
function renderMemories(memories) {
const container = document.getElementById("memories");
if (!memories.length) {
container.innerHTML =
`<div class="memory">
No memories yet. Add something above.
</div>`;
return;
}
container.innerHTML = memories
.slice()
.reverse()
.map(memory => `
<div class="memory">
<div class="memory-type">
${escapeHtml(memory.type || "memory")}
</div>
<div class="memory-title">
${escapeHtml(memory.title || "Memory")}
</div>
<div class="memory-detail">
${escapeHtml(memory.detail || "")}
</div>
</div>
`)
.join("");
}
async function clearMemories() {
if (!confirm("Delete all memories?")) return;
await fetch("/clear", {
method: "POST"
});
loadMemories();
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
loadMemories();
How I Built It
Frontend: plain HTML/CSS/JS
AI: Ollama
Model: Qwen2.5 3B (open-weight)
Backend: Python standard library
Storage: local JSON
Internet: not required once the model is installed
API keys: none
<!DOCTYPE html>
- { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: #0b1020;
color: #f8fafc;
}
.container {
max-width: 900px;
margin: auto;
padding: 50px 20px;
}
.hero {
margin-bottom: 35px;
}
.badge {
display: inline-block;
background: #172554;
color: #93c5fd;
padding: 7px 12px;
border-radius: 999px;
font-size: 13px;
margin-bottom: 15px;
}
h1 {
font-size: 48px;
margin: 0 0 10px;
}
.subtitle {
color: #94a3b8;
font-size: 18px;
line-height: 1.6;
}
.card {
background: #111827;
border: 1px solid #1f2937;
border-radius: 18px;
padding: 22px;
margin-bottom: 20px;
}
textarea,
input {
width: 100%;
background: #020617;
color: white;
border: 1px solid #334155;
border-radius: 12px;
padding: 15px;
font-size: 15px;
outline: none;
}
textarea {
min-height: 150px;
resize: vertical;
}
textarea:focus,
input:focus {
border-color: #6366f1;
}
button {
border: 0;
border-radius: 10px;
padding: 12px 18px;
background: #6366f1;
color: white;
font-weight: 700;
cursor: pointer;
margin-top: 12px;
}
button:hover {
background: #4f46e5;
}
.secondary {
background: #1e293b;
}
.memories {
display: grid;
gap: 12px;
margin-top: 18px;
}
.memory {
background: #020617;
border: 1px solid #1e293b;
padding: 16px;
border-radius: 12px;
}
.memory-type {
color: #818cf8;
text-transform: uppercase;
font-size: 11px;
font-weight: bold;
letter-spacing: 1px;
}
.memory-title {
font-size: 17px;
font-weight: 700;
margin-top: 5px;
}
.memory-detail {
color: #94a3b8;
margin-top: 5px;
}
.answer {
margin-top: 15px;
padding: 15px;
background: #172554;
border-radius: 12px;
color: #bfdbfe;
}
.status {
color: #94a3b8;
margin-top: 10px;
min-height: 20px;
}
.privacy {
text-align: center;
color: #64748b;
margin-top: 30px;
font-size: 13px;
}
LOCAL AI · OPEN WEIGHT · PRIVATE
<h1>🧠 FriendMemory</h1>
A tiny AI memory assistant for someone you care about.
Turn messy messages into memories you can actually find later.
<h2>💬 Remember something</h2>
Remember this
<h2>🔎 Ask your memories</h2>
Ask
<h2>💭 Your memories</h2>
Clear everything
🔒 Your memories stay on your computer.
AI inference happens locally through Ollama.
async function remember() {
const text = document.getElementById("message").value;
const status = document.getElementById("status");
if (!text.trim()) return;
status.textContent = "Thinking...";
try {
const response = await fetch("/remember", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
const data = await response.json();
if (data.error) {
status.textContent = data.error;
return;
}
status.textContent =
`Added ${data.added.length} memory${data.added.length === 1 ? "" : "ies"}.`;
document.getElementById("message").value = "";
renderMemories(data.all);
} catch (error) {
status.textContent =
"Could not connect to the local AI server.";
}
}
async function ask() {
const question = document.getElementById("question").value;
const answerBox = document.getElementById("answer");
if (!question.trim()) return;
answerBox.innerHTML = "Thinking...";
try {
const response = await fetch("/ask", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ question })
});
const data = await response.json();
answerBox.innerHTML =
`<div class="answer">${escapeHtml(data.answer)}</div>`;
} catch (error) {
answerBox.innerHTML =
`<div class="answer">Something went wrong.</div>`;
}
}
async function loadMemories() {
const response = await fetch("/memories");
const memories = await response.json();
renderMemories(memories);
}
function renderMemories(memories) {
const container = document.getElementById("memories");
if (!memories.length) {
container.innerHTML =
`<div class="memory">
No memories yet. Add something above.
</div>`;
return;
}
container.innerHTML = memories
.slice()
.reverse()
.map(memory => `
<div class="memory">
<div class="memory-type">
${escapeHtml(memory.type || "memory")}
</div>
<div class="memory-title">
${escapeHtml(memory.title || "Memory")}
</div>
<div class="memory-detail">
${escapeHtml(memory.detail || "")}
</div>
</div>
`)
.join("");
}
async function clearMemories() {
if (!confirm("Delete all memories?")) return;
await fetch("/clear", {
method: "POST"
});
loadMemories();
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
loadMemories();
Prize Categories
Best Use of Arduino
Top comments (0)