Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ venv
htmlcov
.env
.env.*
*.env
!.env.example
!.env.docker.example
.streamlit/secrets.toml
data
*.db
Expand Down
2 changes: 0 additions & 2 deletions config/docker.env.example → .env.docker.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ APP_PORT=8501
# Host Ollama on Windows: http://host.docker.internal:11434
# Docker Ollama (--profile local-ai): http://ollama:11434
OLLAMA_BASE_URL=http://host.docker.internal:11434
# Must match a model already installed in Ollama.
OLLAMA_MODEL=qwen2.5:1.5b
GEMINI_API_KEY=
GROQ_API_KEY=
CEREBRAS_API_KEY=
Expand Down
1 change: 0 additions & 1 deletion config/local.env.example → .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Optional provider keys. Never commit the real .env file.
DATABASE_URL=sqlite:///data/app.db
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:1.5b
GEMINI_API_KEY=
GROQ_API_KEY=
CEREBRAS_API_KEY=
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: quality

on:
push:
branches: [main, develop, 'feature/**', 'fix/**', 'refactor/**', 'chore/**', 'ci/**']
branches: [main, 'feature/**', 'fix/**', 'refactor/**', 'chore/**', 'ci/**']
pull_request:

permissions:
Expand All @@ -22,8 +22,8 @@ jobs:
with:
python-version: '3.11'
cache: pip
cache-dependency-path: requirements/dev.txt
- run: python -m pip install -r requirements/dev.txt
cache-dependency-path: requirements-dev.txt
- run: python -m pip install -r requirements-dev.txt
- run: python -m pip install --no-build-isolation --no-deps -e ./backend
- run: ruff check .
- run: ruff format --check .
Expand All @@ -45,22 +45,22 @@ jobs:
env:
POSTGRES_USER: fixture
POSTGRES_PASSWORD: fixture-test-only
POSTGRES_DB: study_test
POSTGRES_DB: p0_test
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U fixture -d study_test"
--health-cmd "pg_isready -U fixture -d p0_test"
--health-interval 5s --health-timeout 5s --health-retries 20
env:
DATABASE_URL: postgresql+psycopg2://fixture:fixture-test-only@localhost:5432/study_test
STUDY_TEST_DATABASE_URL: postgresql+psycopg2://fixture:fixture-test-only@localhost:5432/study_test
DATABASE_URL: postgresql+psycopg2://fixture:fixture-test-only@localhost:5432/p0_test
P0_TEST_DATABASE_URL: postgresql+psycopg2://fixture:fixture-test-only@localhost:5432/p0_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: requirements/dev.txt
- run: python -m pip install -r requirements/dev.txt
cache-dependency-path: requirements-dev.txt
- run: python -m pip install -r requirements-dev.txt
- run: python -m pip install --no-build-isolation --no-deps -e ./backend
- run: python -m alembic -c backend/alembic.ini upgrade head
- run: python -m alembic -c backend/alembic.ini check
Expand All @@ -81,7 +81,7 @@ jobs:
cache-to: type=gha,mode=max
- name: Compose and functional smoke without models
run: |
cp config/docker.env.example .env.docker
cp .env.docker.example .env.docker
docker compose --env-file .env.docker config --quiet
docker compose --env-file .env.docker up -d --no-build --wait
curl --fail http://localhost:8501/_stcore/health
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ _venv/
# Tuyệt đối không đẩy file chứa API Keys lên Git
.env
.env.local
.env.*
*.env
!*.env.example
secrets.toml
config.ini

Expand Down
53 changes: 30 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Contributing

Branch from the latest `origin/develop`, use conventional commits and review each
vertical slice separately. Local refactor checkpoints are a dependency stack;
submit/rebase them in plan order after preceding slices merge into develop.
No direct main push or release tag is part of consolidation. Do not commit secrets,
student PDFs, database dumps or model weights.
Keep PRs small and reversible. Branch from the current foundation branch until it
is merged, then from main. Use conventional commits (`fix`, `refactor`, `infra`,
`ci`, `docs`); each correctness change needs a reproducing test. No direct main pushes
or v1 tags in Phase 0. Never include `.env`, real uploaded PDFs, model weights or keys.

PowerShell offline gates from repository root:
Maintainer should configure branch protection for main: require review and the
`quality` workflow's offline/postgres/docker jobs. This document does not configure
GitHub protection settings; remote enforcement must be verified separately.

PowerShell setup:

```powershell
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements/dev.txt
.venv/Scripts/python -m pip install -r requirements-dev.txt
.venv/Scripts/python -m pip install --no-build-isolation --no-deps -e ./backend
.venv/Scripts/python -m pytest -q
.venv/Scripts/ruff check .
Expand All @@ -19,19 +22,23 @@ python -m venv .venv
.venv/Scripts/python -m build --no-isolation ./backend
```

POSIX substitutes `.venv/bin/`. Supported Python is 3.11–3.12. Default tests use
fakes and never download weights or call cloud providers. Ruff covers canonical
code, apps, migrations, scripts, evaluation and all tests. Public contracts/ports
use explicit types; the architecture gate limits vendor imports to reviewed,
deterministic modules and prohibits SDK/network imports in pure layers.

PostgreSQL integration requires a disposable migrated DB: set `DATABASE_URL` and
`STUDY_TEST_DATABASE_URL`, run Alembic upgrade/check, then
`python -m pytest backend/tests/integration -q`. Missing configuration fails this
suite. Tests add/delete fixture records, so never use retained user data.

Use [Docker smoke/backup commands](docs/operations/docker.md) for runtime changes.
Real-model smoke and [evaluation](evaluation/README.md) are opt-in. Record local
results separately from remote CI and human browser acceptance. Review and the
quality workflow's offline/postgres/docker checks should protect develop/main;
the repository files do not configure GitHub branch protection automatically.
POSIX: replace `.venv/Scripts/python` with `.venv/bin/python` and
`.venv/Scripts/ruff` with `.venv/bin/ruff`. Python 3.11–3.12 is supported.
Run from repository root. Dev lock provides offline test dependencies; it does not
install Torch/Chroma or download weights. Docker supplies the complete CPU runtime.
Root pytest explicitly selects offline suites; online evaluation scripts are opt-in.

Ruff applies to P0 new/touched code; untouched legacy modules will be brought into
the gate as they are extracted in P1. Current typing policy: annotate new contracts,
ports and public core functions, validate DTOs at runtime; static type checking is
deferred until legacy adapters have typed boundaries.

PostgreSQL integration: create a disposable database, set both DATABASE_URL and
P0_TEST_DATABASE_URL, run Alembic upgrade/check, then
`python -m pytest backend/tests/integration -q`. Missing DB configuration fails this
suite explicitly. Never point it at retained user data.

Full embedding/Ollama/browser smoke is a separate acceptance check with recorded
model/environment metadata. Fast CI must not call cloud LLMs or require GPU access.
See [Docker](docs/operations/docker.md), [migrations](docs/operations/migrations.md)
and [acceptance evidence](docs/reports/p0-acceptance.md).
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ WORKDIR /app
RUN apt-get update \
&& apt-get install --no-install-recommends -y libgomp1 \
&& rm -rf /var/lib/apt/lists/*
COPY requirements/runtime.txt ./requirements/runtime.txt
COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
&& python -m pip install -r requirements/runtime.txt
&& python -m pip install -r requirements.txt
RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin study \
&& mkdir -p /app/data /home/study/.cache/huggingface \
&& chown -R study:study /app/data /home/study
Expand Down
61 changes: 37 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,60 @@
# Self Study Supporter

Ứng dụng tự học từ PDF: đăng ký/đăng nhập, BGE-M3 semantic chunking, hybrid
Dense/BM25/RRF, ReAct chat kèm nguồn, quiz/progress và concept graph.
Code nghiệp vụ nằm trong `backend/src/study_supporter`; Streamlit ở
`apps/streamlit_legacy` gọi application services qua bootstrap.
Ứng dụng hỗ trợ tự học từ PDF, hiện dùng Streamlit, PostgreSQL/SQLite và Chroma.
Nhánh `feature/docker-local-foundation` xây dựng nền tảng kỹ thuật Phase 0.

Chạy Docker từ repository root (PowerShell):
Đã triển khai: đăng ký/đăng nhập; PDF có text layer; semantic chunking với BGE-M3;
hybrid retrieval Dense + BM25 + RRF; custom ReAct với công cụ tìm tài liệu, quiz,
ghi chú và tiến độ; concept graph/lộ trình học; quiz lưu SQL; persistent Chroma.
Quiz chỉ được chấm khi có đúng bốn lựa chọn và một đáp án hợp lệ. PDF scan cần OCR.

Next.js, FastAPI, LangGraph, reranker và cá nhân hóa mastery thuộc roadmap, chưa
triển khai trong Phase 0. Chưa phát hành v1.0.0; chưa có benchmark chất lượng LLM.

## Chạy bằng Docker (PowerShell)

```powershell
Copy-Item config/docker.env.example .env.docker
# Đặt POSTGRES_PASSWORD; cấu hình Ollama hoặc provider nếu cần chat/quiz.
git clone https://github.com/ManhT005/self-study_supporter.git
cd self-study_supporter
git switch feature/docker-local-foundation
Copy-Item .env.docker.example .env.docker
# Đổi POSTGRES_PASSWORD. Có thể để trống các key cloud.
docker compose --env-file .env.docker config --quiet
docker compose --env-file .env.docker up -d --build --wait
```

Mở http://localhost:8501. Docker chạy migration trước app và giữ SQL/Chroma/model
cache trong volume. BGE-M3 tải khi ingest lần đầu; chat/quiz cần model đã có ở
Ollama hoặc cloud provider. PDF scan cần OCR; quiz phải có bốn lựa chọn hợp lệ.
Mở http://localhost:8501. Docker dùng Python 3.11/CPU và chạy migration trước app.
App khỏe không đồng nghĩa model đã sẵn sàng. BGE-M3 tải vào `hf_cache` khi ingest;
chat/quiz cần Ollama đã có model hoặc provider được cấu hình. Không tải model khi build.
Đọc [Docker/Ollama/backup](docs/operations/docker.md) trước khi thao tác dữ liệu.
POSIX dùng `cp` thay `Copy-Item`; các lệnh Compose giống nhau.

## Kiểm thử và phát triển

Phát triển/test offline, Python 3.11–3.12:
Python 3.11–3.12; dùng môi trường riêng. Bộ test nhanh không gọi model hoặc cloud.

```powershell
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements/dev.txt
.venv/Scripts/python -m pip install -r requirements-dev.txt
.venv/Scripts/python -m pip install --no-build-isolation --no-deps -e ./backend
.venv/Scripts/python -m pytest -q
.venv/Scripts/ruff check .
.venv/Scripts/ruff format --check .
.venv/Scripts/python scripts/check_import_boundaries.py
```

POSIX dùng `.venv/bin/`. Native UI cần runtime dependencies và migration; xem
[env mẫu](config/README.md) và [dependency locks](requirements/README.md).
`main.py` và `eval_test.py` là command redirects để giữ lệnh cũ hoạt động.
`backend/` là distribution `study-supporter` theo src-layout, không có runtime
framework dependency. Legacy `src/` và `ui/` được giữ lại; RRF forward sang package
mới và có golden tests. Không đổi thuật toán, model, vector dimension hoặc ID collection.
Các nhóm import được kiểm tra bằng AST; core không import UI, driver hoặc SDK.

- [Kiến trúc](docs/architecture/overview.md) và [ADR consolidation](docs/architecture/adr/repository-consolidation.md)
- [Đóng góp và command matrix](CONTRIBUTING.md)
- [Docker/Ollama/backup](docs/operations/docker.md) và [migrations](docs/operations/migrations.md)
- [Kết quả refactor](docs/reports/repository-consolidation-acceptance.md)
- [Evaluation opt-in](evaluation/README.md)
- [Hướng dẫn đóng góp và command matrix](CONTRIBUTING.md)
- [Kiến trúc và ADR](docs/architecture/overview.md)
- [Kế hoạch Phase 0](docs/plans/PHASE_0_ENGINEERING_FOUNDATION.md)
- [Migration DB trống và adoption DB cũ](docs/operations/migrations.md)
- [Contract cho frontend tương lai](docs/api/README.md)
- [Kế hoạch foundation lịch sử](docs/plans/PHASE_0_ENGINEERING_FOUNDATION.md) và [bằng chứng cũ](docs/reports/p0-acceptance.md)
- [Kết quả nghiệm thu thực tế và phần còn thiếu](docs/reports/p0-acceptance.md)

Không commit env thật, PDF người dùng, dumps hoặc model weights. `down`/`up` giữ
volume; tránh `down -v` với dữ liệu cần giữ. Next.js/FastAPI/LangGraph và thay đổi
retrieval thuộc workstream riêng; consolidation không thay schema/model/vector IDs.
Không commit `.env`, tài liệu người dùng, Chroma/SQL dumps hay model weights.
Dùng `down`/`up` để giữ volume; không dùng `down -v` với dữ liệu cần giữ.
DB cũ chưa có revision Alembic cần backup, schema comparison và review trước adoption.
2 changes: 0 additions & 2 deletions docs/ai-study-agent.md → ai-study-agent.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
> Historical project proposal. Current layout and commands: [README](../README.md) and [architecture](architecture/overview.md).

# TÀI LIỆU THIẾT KẾ & KẾ HOẠCH TRIỂN KHAI DỰ ÁN: AI STUDY AGENT

**Vị trí định hướng:** AI Agent & Algorithm (Chương trình Sinh viên Công nghệ Tập sự)
Expand Down
28 changes: 13 additions & 15 deletions apps/streamlit_legacy/components/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def render_chat():
for msg in st.session_state.chat_history:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
_render_sources(msg.get("evidences", ()))
else:
st.markdown("_Nhập câu hỏi để bắt đầu cuộc hội thoại._")

Expand Down Expand Up @@ -65,18 +64,17 @@ def _handle_message(user_question: str) -> None:
return

st.markdown(result.answer)
_render_sources(result.evidences)
evidence = result.evidences
sources = [
f"{item.source} · trang {item.page_start}-{item.page_end} · {item.chunk_id}"
for item in evidence
]
chip_html = "".join(
f"<span class='chip'>{html.escape(source)}</span>" for source in sources
)
st.markdown(
f"<div style='margin-top:0.45rem;'>{chip_html}</div>",
unsafe_allow_html=True,
)

st.session_state.chat_history.append(
{"role": "assistant", "content": result.answer, "evidences": result.evidences}
)


def _render_sources(evidences):
sources = [
f"{item.source} · trang {item.page_start}-{item.page_end} · {item.chunk_id}"
for item in evidences
]
if sources:
chips = "".join(f"<span class='chip'>{html.escape(source)}</span>" for source in sources)
st.markdown(f"<div style='margin-top:0.45rem;'>{chips}</div>", unsafe_allow_html=True)
st.session_state.chat_history.append({"role": "assistant", "content": result.answer})
3 changes: 1 addition & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# study-supporter

Python 3.11–3.12, src layout. Install with `python -m pip install -e ./backend`.
Core installs only deterministic NumPy/Pydantic/NetworkX dependencies. Runtime
drivers are in the runtime extra; Streamlit lives in apps/streamlit_legacy.
Core has no runtime dependencies. Legacy adapters forward to unchanged pure RRF.
Contracts require authenticated identity; ownership is checked before retrieval.
Run `python scripts/check_import_boundaries.py` and `python -m pytest` from repo root.
40 changes: 17 additions & 23 deletions backend/migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
"""Explicit migration lifecycle; never load dotenv or print credentials."""
"""Explicit migrations; do not load .env or print connection URLs."""

import os
from alembic import context
from study_supporter.config import Settings
from study_supporter.infrastructure.persistence.sqlalchemy.database import Base, Database
from study_supporter.infrastructure.persistence.sqlalchemy import models # noqa: F401

from src.db.database import Base, engine
from src.db import models # noqa: F401: register models for metadata comparison

target_metadata = Base.metadata
database = Database(Settings.from_mapping(os.environ))
try:
if context.is_offline_mode():
context.configure(
url=database.engine.url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

if context.is_offline_mode():
context.configure(
url=engine.url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
else:
with engine.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
else:
with database.engine.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata, compare_type=True
)
with context.begin_transaction():
context.run_migrations()
finally:
database.close()
5 changes: 2 additions & 3 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ name = "study-supporter"
version = "0.0.0"
description = "Framework-independent core of Self Study Supporter"
requires-python = ">=3.11,<3.13"
dependencies = ["numpy==1.26.4", "pydantic==2.10.4", "networkx==3.4.2"]
dependencies = []

[project.optional-dependencies]
dev = ['pytest==8.3.4', 'ruff==0.9.10', 'build==1.2.2.post1', 'setuptools==75.6.0', 'wheel==0.45.1', 'requests==2.32.5', 'sqlalchemy==2.0.36', 'streamlit==1.41.1', 'pymupdf==1.25.1', 'python-dotenv==1.0.1', 'bcrypt==4.2.1', 'alembic==1.14.0', 'psycopg2-binary==2.9.10', 'google-genai==1.56.0', 'rank-bm25==0.2.2']
runtime = ['google-genai==1.56.0', 'python-dotenv==1.0.1', 'pymupdf==1.25.1', 'chromadb==0.5.23', 'posthog==3.7.0', 'rank-bm25==0.2.2', 'sentence-transformers==3.3.1', 'transformers==4.46.3', 'torch==2.5.1', 'sqlalchemy==2.0.36', 'streamlit==1.41.1', 'requests==2.32.5', 'bcrypt==4.2.1', 'psycopg2-binary==2.9.10', 'alembic==1.14.0']
dev = ["pytest==8.3.4", "ruff==0.9.10", "build==1.2.2.post1"]

[tool.setuptools.packages.find]
where = ["src"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import uuid
from study_supporter.application.contracts.learning import AnswerResult
from study_supporter.domain.errors import EvidenceNotFound


class AnswerQuestionUseCase:
def __init__(self, runner, evidence_tool):
self.runner, self.evidence_tool = runner, evidence_tool

def execute(self, request):
if request.context != self.evidence_tool.context:
raise EvidenceNotFound("Document not found")
self.evidence_tool.last_evidence = []
state = self.runner.run(request.question)
return AnswerResult(
Expand Down
Loading
Loading