personaflow / docs /SYSTEM_DESIGN.md
mozzic's picture
Deploy PersonaFlow AI
9d1557c verified
|
Raw
History Blame Contribute Delete
7.42 kB

System Design

Runtime topology

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    HTTP    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
client ── uvicorn :8000 β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  FastAPI (app/main.py)           β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚   /simulate-review               β”‚
                                  β”‚   /recommend                     β”‚
                                  β”‚   /persona  /users  /health      β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                  β”‚
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚                                                 β”‚
            β–Ό                                                 β–Ό
   Persona/Memory store                              Groq LLM API
   (filesystem JSON cache)                           (HTTPS, llama-3.1/3.3)
   data/personas/*.json                                   ^      ^
   data/memory/*.json                                     β”‚      β”‚
            β”‚                                             β”‚      β”‚
            β–Ό                                             β”‚      β”‚
   Processed parquet                                      β”‚      β”‚
   data/processed/reviews.parquet                         β”‚      β”‚
                  items.parquet                           β”‚      β”‚
                  users.parquet                           β”‚      β”‚
            β”‚                                             β”‚      β”‚
            β–Ό                                             β”‚      β”‚
   Trained artifacts                                      β”‚      β”‚
   models_store/rating_lgbm.pkl   (LightGBM regressor) β”€β”€β”€β”˜      β”‚
   models_store/item_index.pkl    (TF-IDF + items df)            β”‚
   models_store/item_sim.pkl      (item-item cosine sparse) β”€β”€β”€β”€β”€β”˜

Layered components

Layer File Purpose LLM-bound?
HTTP app/main.py FastAPI routes, request validation no
Orchestration app/recommender.py Task B pipeline coordinator no (delegates)
Reasoning Planner app/planner.py Per-request strategy + ranking weights yes (1 call)
Behavioral Analyzer app/recommender.py:analyze_behavior Decode current decision state yes (1 call)
Behavioral Simulator app/reasoner.py Per-candidate predicted reaction yes (N calls)
Review Generator app/generator.py Final user-facing review text yes (1 call per Task A request)
Persona builder app/persona/features.py + refine.py + store.py Stats + LLM refinement + cache mixed (LLM once per user, then cached)
Memory app/memory.py Long-term + short-term + tagged experiences no
Context app/context.py Time bucket + auto NG flags no
Retrieval app/retrieval.py TF-IDF + item-item CF no
Rating model app/rating_model.py LightGBM cross-check no
LLM client app/llm.py Groq wrapper with JSON-mode no
Config app/config.py Paths + env loading no

Data flow per request

POST /simulate-review

request β†’ persona_for(user_id, refine=True)  # cached after first call
       β†’ memory.get_or_build(user_id)         # cached after first build
       β†’ context.normalize(request.context)   # auto NG flags
       β†’ reasoner.reason(persona, memory, context, item)   # LLM call
       β†’ rating_model.predict(persona, item)               # LightGBM
       β†’ blend rating (0.6 LLM + 0.4 LGBM)
       β†’ generator.generate(persona, item, reasoner_out, context)  # LLM call
       β†’ response

Net: 2 LLM calls + 1 LightGBM inference per request after warmup.

POST /recommend

request β†’ persona + memory + context  (same as above)
       β†’ analyze_behavior(...)         # LLM call 1
       β†’ plan(...)                     # LLM call 2 β€” strategy
       β†’ retrieve(user_id, top_k=12)   # local TF-IDF + CF
       β†’ for each candidate (12):
             reason(persona, memory, context, candidate)  # LLM call 3..14
             rating_model.predict(persona, candidate)
       β†’ rerank
       β†’ response (top_n)

Net: 2 + N LLM calls per request (N = candidates_k, default 12).

Caching

  • Persona JSON: written to data/personas/{user_id}.json after first build. The LLM-refinement call is the expensive part; once written, all subsequent requests for that user use the cache.
  • Memory JSON: written to data/memory/{user_id}.json after first build. Currently NOT auto-rebuilt; see "Cache invalidation" below.
  • TF-IDF + CF: built once via python -m app.retrieval and pickled to models_store/. The @lru_cache on _bundle() loads it once per process.
  • LightGBM model: pickled to models_store/rating_lgbm.pkl and loaded on first predict() call.

Cache invalidation

Today: filesystem caches are not auto-invalidated. To rebuild:

# Persona/memory after a schema change
Remove-Item -Recurse data/personas/*.json, data/memory/*.json

# TF-IDF + CF after the dataset changes
Remove-Item models_store/item_index.pkl, models_store/item_sim.pkl
python -m app.retrieval

# LightGBM after feature changes
Remove-Item models_store/rating_lgbm.pkl
python -m app.rating_model

Production version should hash the persona schema + git commit and invalidate on mismatch.

Scalability

Concern Current Path to scale
Per-candidate LLM cost 12 calls per /recommend Async fan-out to Groq (groq SDK supports asyncio); or distill into a small local reasoner
Persona LLM cost 1 per cold user Already cached on disk; for >100k users move to Redis/Postgres
TF-IDF + CF index size ~250 MB combined Move to a vector DB (Qdrant/Milvus) for >1M items
Rating model retraining Manual (python -m app.rating_model) Schedule weekly via cron; track RMSE drift
LLM rate limits (Groq) Single API key Pool keys + retry-with-backoff in app/llm.py

Failure modes & current handling

Failure Behavior
Groq returns invalid JSON chat_json salvages by stripping code fences; on second failure raises (caller's except returns heuristic fallback)
User not in dataset 404 from /persona, /simulate-review, /recommend
Parquet missing 503 with instruction to run python -m app.data.ingest
LightGBM model missing rating model returns None; LLM rating is used alone
LLM rate limit Currently surfaces as HTTP 5xx; add retry in app/llm.py for prod

Deployment

docker-compose.yml ships a single-service image. The first container start runs app/data/ingest.py + app/retrieval.py + app/rating_model.py as an init job, then starts uvicorn. Data and model artifacts are mounted as volumes so subsequent boots skip the build.

Resource budget for an 8-core dev box:

  • RAM: ~1.5 GB peak during ingest, ~600 MB serving
  • Disk: ~700 MB (raw CSV + parquet + indexes + models)
  • CPU: ingest is ~3 min; serving is LLM-bound (CPU near-idle)