Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ You're done! Access the app at `localhost:8501` in your browser.

The recommender system works by loading a set of random video links. Once the user hits the Run button, a video will be shown, and the system will suggest whether it recommends the video or not. The user can then provide feedback using "pain" or "pleasure" signals to guide the recommendation process. Based on this feedback, the system adjusts its responses and suggests another video. This cycle continues, allowing for more accurate and personalized recommendations over time.

## Book Domain Demo

This fork also includes a local book recommendation domain that can run without paid APIs:

```bash
streamlit run book_recommender.py
```

The demo uses `book_domain.py` for a small local catalog and deterministic fallback ranking. If `ao_core` and `ao_arch` are installed, `book_recommender.py` will also initialize an AO agent using `arch__BookRecommender.py`; otherwise it remains runnable with the fallback path.

Validation:

```bash
python -m unittest discover -s tests
python -m py_compile book_domain.py book_recommender.py arch__BookRecommender.py tests/test_book_domain.py
```


## Contributing

Expand Down
19 changes: 19 additions & 0 deletions arch__BookRecommender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Architecture definition for the book-domain recommender demo.

The input vector is built in book_domain.encode_book:
genre + pace + length + tone + format + current reader mood.
"""

import ao_arch as ar
from book_domain import binary_input_size

description = "Book recommender for local catalog and reader context"

arch_i = [binary_input_size()]
arch_z = [10]
arch_c = []
connector_function = "full_conn"

arch = ar.Arch(arch_i, arch_z, arch_c, connector_function, description)
157 changes: 157 additions & 0 deletions book_domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
BOOK_CATALOG = [
{
"id": "project-hail-mary",
"title": "Project Hail Mary",
"genre": "science_fiction",
"pace": "fast",
"length": "long",
"tone": "hopeful",
"format": "novel",
"context": ["curious", "adventurous"],
},
{
"id": "atomic-habits",
"title": "Atomic Habits",
"genre": "self_improvement",
"pace": "medium",
"length": "medium",
"tone": "practical",
"format": "nonfiction",
"context": ["focused", "motivated"],
},
{
"id": "the-house-in-the-cerulean-sea",
"title": "The House in the Cerulean Sea",
"genre": "fantasy",
"pace": "gentle",
"length": "medium",
"tone": "cozy",
"format": "novel",
"context": ["comfort", "relaxed"],
},
{
"id": "educated",
"title": "Educated",
"genre": "memoir",
"pace": "medium",
"length": "medium",
"tone": "reflective",
"format": "nonfiction",
"context": ["thoughtful", "resilient"],
},
{
"id": "the-murderbot-diaries",
"title": "All Systems Red",
"genre": "science_fiction",
"pace": "fast",
"length": "short",
"tone": "witty",
"format": "novella",
"context": ["fun", "adventurous"],
},
{
"id": "braiding-sweetgrass",
"title": "Braiding Sweetgrass",
"genre": "nature",
"pace": "gentle",
"length": "long",
"tone": "reflective",
"format": "nonfiction",
"context": ["thoughtful", "calm"],
},
{
"id": "the-way-of-kings",
"title": "The Way of Kings",
"genre": "fantasy",
"pace": "slow",
"length": "long",
"tone": "epic",
"format": "novel",
"context": ["immersive", "adventurous"],
},
{
"id": "deep-work",
"title": "Deep Work",
"genre": "productivity",
"pace": "medium",
"length": "medium",
"tone": "practical",
"format": "nonfiction",
"context": ["focused", "motivated"],
},
{
"id": "legends-and-lattes",
"title": "Legends & Lattes",
"genre": "fantasy",
"pace": "gentle",
"length": "short",
"tone": "cozy",
"format": "novel",
"context": ["comfort", "fun"],
},
{
"id": "the-design-of-everyday-things",
"title": "The Design of Everyday Things",
"genre": "design",
"pace": "medium",
"length": "medium",
"tone": "practical",
"format": "nonfiction",
"context": ["curious", "focused"],
},
]

GENRES = [
"science_fiction",
"fantasy",
"self_improvement",
"memoir",
"nature",
"productivity",
"design",
]
PACES = ["gentle", "medium", "fast", "slow"]
LENGTHS = ["short", "medium", "long"]
TONES = ["cozy", "hopeful", "practical", "reflective", "witty", "epic"]
FORMATS = ["novel", "novella", "nonfiction"]
MOODS = ["comfort", "curious", "focused", "fun", "adventurous", "thoughtful", "motivated", "relaxed", "calm", "immersive"]


def one_hot(value, choices):
return [1 if value == choice else 0 for choice in choices]


def encode_book(book, mood="curious"):
return (
one_hot(book["genre"], GENRES)
+ one_hot(book["pace"], PACES)
+ one_hot(book["length"], LENGTHS)
+ one_hot(book["tone"], TONES)
+ one_hot(book["format"], FORMATS)
+ one_hot(mood, MOODS)
)


def score_book(book, preferences):
score = 0
for field in ("genre", "pace", "length", "tone", "format"):
wanted = preferences.get(field)
if wanted and book[field] == wanted:
score += 2
mood = preferences.get("mood")
if mood and mood in book["context"]:
score += 3
return score


def recommend_books(preferences, catalog=None, limit=3):
catalog = catalog or BOOK_CATALOG
ranked = sorted(
catalog,
key=lambda book: (-score_book(book, preferences), book["title"]),
)
return ranked[:limit]


def binary_input_size():
return len(encode_book(BOOK_CATALOG[0]))
77 changes: 77 additions & 0 deletions book_recommender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import streamlit as st

from book_domain import BOOK_CATALOG, encode_book, recommend_books

try:
import ao_core as ao
from arch__BookRecommender import arch
except Exception:
ao = None
arch = None


def get_agent():
if ao is None or arch is None:
return None
if "book_agent" not in st.session_state:
st.session_state.book_agent = ao.Agent(arch, notes="Book Recommender Agent")
for _ in range(4):
st.session_state.book_agent.reset_state()
st.session_state.book_agent.reset_state(training=True)
return st.session_state.book_agent


def agent_vote(agent, book, mood):
if agent is None:
return None
agent.reset_state()
response = agent.next_state(encode_book(book, mood))
if not response:
return 0
return round(sum(response) / len(response) * 100, 1)


st.set_page_config(page_title="AO Book Recommender")
st.title("AO Book Recommender")

st.caption(
"A local book-domain demo for applying the AO recommender pattern without paid APIs."
)

genre = st.selectbox(
"Preferred genre",
["science_fiction", "fantasy", "self_improvement", "memoir", "nature", "productivity", "design"],
)
pace = st.selectbox("Reading pace", ["gentle", "medium", "fast", "slow"])
length = st.selectbox("Book length", ["short", "medium", "long"])
tone = st.selectbox("Tone", ["cozy", "hopeful", "practical", "reflective", "witty", "epic"])
book_format = st.selectbox("Format", ["novel", "novella", "nonfiction"])
mood = st.selectbox(
"Current reading context",
["comfort", "curious", "focused", "fun", "adventurous", "thoughtful", "motivated", "relaxed", "calm", "immersive"],
)

preferences = {
"genre": genre,
"pace": pace,
"length": length,
"tone": tone,
"format": book_format,
"mood": mood,
}

agent = get_agent()
if agent is None:
st.info("AO packages are not installed; using deterministic local fallback scoring.")

for book in recommend_books(preferences, BOOK_CATALOG, limit=3):
vote = agent_vote(agent, book, mood)
with st.container():
st.subheader(book["title"])
st.write(
f"{book['genre']} · {book['pace']} pace · {book['length']} · "
f"{book['tone']} · {book['format']}"
)
st.write("Reader contexts: " + ", ".join(book["context"]))
if vote is not None:
st.write(f"AO recommendation signal: {vote}%")
46 changes: 46 additions & 0 deletions tests/test_book_domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest

from book_domain import BOOK_CATALOG, binary_input_size, encode_book, recommend_books


class BookDomainTests(unittest.TestCase):
def test_encoding_has_stable_binary_shape(self):
encoded = encode_book(BOOK_CATALOG[0], mood="curious")

self.assertEqual(len(encoded), binary_input_size())
self.assertTrue(all(bit in (0, 1) for bit in encoded))

def test_recommendation_prioritizes_matching_context(self):
results = recommend_books(
{
"genre": "fantasy",
"pace": "gentle",
"length": "short",
"tone": "cozy",
"format": "novel",
"mood": "comfort",
},
limit=1,
)

self.assertEqual(results[0]["id"], "legends-and-lattes")

def test_recommendation_supports_nonfiction_focus(self):
results = recommend_books(
{
"genre": "productivity",
"pace": "medium",
"length": "medium",
"tone": "practical",
"format": "nonfiction",
"mood": "focused",
},
limit=2,
)

self.assertEqual(results[0]["id"], "deep-work")
self.assertTrue(all(book["format"] == "nonfiction" for book in results))


if __name__ == "__main__":
unittest.main()