Commit bd79f85f authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev_chapters' into 'dev'

Dev chapters

See merge request kantz/tutor_react!5
parents 98cd81ec f77b87ae
......@@ -2,17 +2,17 @@ from app.deterministic_services import llm_client
HINT_SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor."
1) Antworte NUR mit einem kurzen Tipp (1-2 Sätze), keine Beispiele, keine Herleitung, keine komplette Lösung.
2) Beziehe dich PRIMÄR auf die 'Aktuelle Frage'. Ignoriere ältere Nebenfragen, außer sie sind nötig.
3) Wenn der Nutzer etwas Falsches sagt weiße ihn darauf hin gib aber keine Lösung an.
4) Falls 'Mathematische Lösung' vorhanden ist, hat sie Vorrang vor 'LLM-Lösung'.
verwende die $ für mathematische Formeln, z.B. $a^2 + b^2 = c^2$
Antworte mit folgender Struktur
Das war richtig./Das ist noch nicht richtig./Gute Frage!
[Hier soll der nächster Tipp um weiter zu machen/um den Fehler zu korrigieren stehen ohne Klammern]
Du bist ein didaktischer Mathe-Tutor.
1) Antworte NUR mit EINEM Satz (max. 20 Wörter), der den Tipp enthält. Keine Beispiele, keine Herleitungen, keine Lösungen.
2) Beziehe dich IMMER auf die 'Aktuelle Frage'. Ignoriere ältere Fragen, außer sie sind DIREKT relevant (z. B. Definitionen).
3) Wenn der Nutzer etwas Falsches sagt, antworte: "Das ist noch nicht richtig. Prüfe [konkreter Aspekt]."
4) Falls eine 'Mathematische Lösung' (Formel/Algorithmus) existiert, gib NUR diese als Tipp an – selbst wenn der Nutzer nach Alternativen fragt.
5) Verwende IMMER die $-Notation für Formeln (z. B. $a^2 + b^2 = c^2$). Kein LaTeX außerhalb der $-Blöcke.
Antworte IMMER in dieser Form:
- "Das war richtig. [Tipp in einem Satz]."
- "Das ist noch nicht richtig. [Tipp in einem Satz]."
- "Gute Frage! [Tipp in einem Satz]."
"""
def generate_hint(
......
......@@ -59,16 +59,19 @@ def parse_markdown_with_frontmatter(text: str) -> Tuple[Dict[str, Any], str]:
def stable_uid(doc_type: str, path: str, meta: Dict[str, Any]) -> str:
chapter = meta.get("chapter_index")
sec = meta.get("section_index")
sub = meta.get("subsection_index")
child = meta.get("child_index")
if doc_type == "section" and sec is not None:
raw = f"section|s{int(sec):03d}"
elif doc_type == "subsection" and sec is not None and sub is not None:
raw = f"subsection|s{int(sec):03d}|ss{int(sub):03d}"
elif doc_type == "child" and sec is not None and sub is not None and child is not None:
raw = f"child|s{int(sec):03d}|ss{int(sub):03d}|c{int(child):03d}"
if doc_type == "chapter" and chapter is not None:
raw = f"chapter|c{int(chapter):03d}"
if doc_type == "section" and chapter is not None and sec is not None:
raw = f"section|c{int(chapter):03d}|s{int(sec):03d}"
elif doc_type == "subsection" and chapter is not None and sec is not None and sub is not None:
raw = f"subsection|c{int(chapter):03d}|s{int(sec):03d}|ss{int(sub):03d}"
elif doc_type == "child" and chapter is not None and sec is not None and sub is not None and child is not None:
raw = f"child|c{int(chapter):03d}|s{int(sec):03d}|ss{int(sub):03d}|c{int(child):03d}"
else:
raw = f"{doc_type}|{path}"
......@@ -79,6 +82,7 @@ def stable_uid(doc_type: str, path: str, meta: Dict[str, Any]) -> str:
def load_docs(base_dir: Path) -> List[DocRecord]:
docs: List[DocRecord] = []
mapping = [
("chapter", [base_dir / "chapters", base_dir / "chapter"]),
("section", [base_dir / "sections", base_dir / "section"]),
("subsection", [base_dir / "subsections", base_dir / "subsection"]),
("child", [base_dir / "childs"]),
......@@ -110,7 +114,7 @@ def load_docs(base_dir: Path) -> List[DocRecord]:
if not docs:
raise FileNotFoundError(
f"No markdown docs found under {base_dir}. Expected sections/subsections/childs."
f"No markdown docs found under {base_dir}. Expected chapter/sections/subsections/childs."
)
return docs
......@@ -126,10 +130,12 @@ CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS docs (
uid TEXT PRIMARY KEY,
doc_type TEXT NOT NULL,
chapter_index INT NULL,
section_index INT NULL,
subsection_index INT NULL,
child_index INT NULL,
chapter_title TEXT NULL,
section_title TEXT NULL,
subsection_title TEXT NULL,
title TEXT NULL,
......@@ -146,8 +152,9 @@ CREATE INDEX IF NOT EXISTS docs_embedding_cos_idx
CREATE INDEX IF NOT EXISTS docs_source_type_idx ON docs(source_type);
CREATE INDEX IF NOT EXISTS docs_doc_type_idx ON docs(doc_type);
CREATE INDEX IF NOT EXISTS docs_section_idx ON docs(section_index);
CREATE INDEX IF NOT EXISTS docs_subsection_idx ON docs(section_index, subsection_index);
CREATE INDEX IF NOT EXISTS docs_chapter_idx ON docs(chapter_index);
CREATE INDEX IF NOT EXISTS docs_section_idx ON docs(chapter_index, section_index);
CREATE INDEX IF NOT EXISTS docs_subsection_idx ON docs(chapter_index, section_index, subsection_index);
"""
......@@ -166,20 +173,22 @@ def init_db(pg_url: str) -> None:
UPSERT_SQL = """
INSERT INTO docs (
uid, doc_type,
section_index, subsection_index, child_index,
section_title, subsection_title, title, source_type,
chapter_index, section_index, subsection_index, child_index,
chapter_title, section_title, subsection_title, title, source_type,
path, markdown, embedding
) VALUES (
%(uid)s, %(doc_type)s,
%(section_index)s, %(subsection_index)s, %(child_index)s,
%(section_title)s, %(subsection_title)s, %(title)s, %(source_type)s,
%(chapter_index)s, %(section_index)s, %(subsection_index)s, %(child_index)s,
%(chapter_title)s, %(section_title)s, %(subsection_title)s, %(title)s, %(source_type)s,
%(path)s, %(markdown)s, %(embedding)s
)
ON CONFLICT (uid) DO UPDATE SET
doc_type = EXCLUDED.doc_type,
chapter_index = EXCLUDED.chapter_index,
section_index = EXCLUDED.section_index,
subsection_index = EXCLUDED.subsection_index,
child_index = EXCLUDED.child_index,
chapter_title = EXCLUDED.chapter_title,
section_title = EXCLUDED.section_title,
subsection_title = EXCLUDED.subsection_title,
title = EXCLUDED.title,
......@@ -212,9 +221,11 @@ def upsert_docs(pg_url: str, docs: List[DocRecord], embeddings: List[List[float]
{
"uid": doc.uid,
"doc_type": doc.doc_type,
"chapter_index": _meta_int(m, "chapter_index"),
"section_index": _meta_int(m, "section_index"),
"subsection_index": _meta_int(m, "subsection_index"),
"child_index": _meta_int(m, "child_index"),
"chapter_title": m.get("chapter_title"),
"section_title": m.get("section_title"),
"subsection_title": m.get("subsection_title"),
"title": m.get("title"),
......@@ -274,9 +285,11 @@ def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) ->
meta = {
"uid": row["uid"],
"doc_type": row["doc_type"],
"chapter_index": row["chapter_index"],
"section_index": row["section_index"],
"subsection_index": row["subsection_index"],
"child_index": row["child_index"],
"chapter_title": row["chapter_title"],
"section_title": row["section_title"],
"subsection_title": row["subsection_title"],
"title": row["title"],
......@@ -297,6 +310,7 @@ def retrieve(
embedder: EmbeddingLike,
query: str,
k: int = 4,
chapter_index: Optional[int] = None,
section_index: Optional[int] = None,
subsection_index: Optional[int] = None,
source_type_filter: Optional[List[str]] = None,
......@@ -304,9 +318,14 @@ def retrieve(
neighbor_expand: int = 0,
) -> List[Source]:
qvec = Vector(embed_query(embedder, query))
vector_k = max(k * 4, k + 16)
where = ["doc_type = 'child'"]
params: Dict[str, Any] = {"qvec": qvec, "k": k}
params: Dict[str, Any] = {"qvec": qvec, "vector_k": vector_k}
if chapter_index is not None:
where.append("chapter_index = %(chapter_index)s")
params["chapter_index"] = chapter_index
if section_index is not None:
where.append("section_index = %(section_index)s")
......@@ -325,60 +344,74 @@ def retrieve(
sql = f"""
SELECT
uid, doc_type,
section_index, subsection_index, child_index,
section_title, subsection_title, title, source_type,
chapter_index, section_index, subsection_index, child_index,
chapter_title, section_title, subsection_title, title, source_type,
path, markdown,
1 - (embedding <=> %(qvec)s) AS score
FROM docs
WHERE {where_sql}
ORDER BY embedding <=> %(qvec)s
LIMIT %(k)s;
LIMIT %(vector_k)s;
"""
children: List[Retrieved] = []
neighbors: List[Retrieved] = []
chapter_docs: List[Retrieved] = []
subsections: List[Retrieved] = []
sections_docs: List[Retrieved] = []
children_direct: List[Retrieved] = []
children_expanded: List[Retrieved] = []
def _top_k_unique(values: List[Retrieved], wanted_k: int) -> List[Retrieved]:
selected: List[Retrieved] = []
seen: set[str] = set()
for item in values:
if item.uid in seen:
continue
selected.append(item)
seen.add(item.uid)
if len(selected) >= wanted_k:
break
return selected
with psycopg.connect(pg_url, row_factory=dict_row) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute(sql, params)
rows = cur.fetchall()
children = [_row_to_retrieved(row) for row in rows]
children_direct = list(children)
children_direct = _top_k_unique(children, k)
if expand_links and children:
sec_sub_counts: Dict[tuple[int, int], int] = {}
for child in children:
if expand_links and children_direct:
cpt_sec_sub_counts: Dict[tuple[int, int, int], int] = {}
for child in children_direct:
cpt_idx = child.metadata.get("chapter_index")
sec_idx = child.metadata.get("section_index")
sub_idx = child.metadata.get("subsection_index")
if sec_idx is not None and sub_idx is not None:
key = (int(sec_idx), int(sub_idx))
sec_sub_counts[key] = sec_sub_counts.get(key, 0) + 1
if cpt_idx is not None and sec_idx is not None and sub_idx is not None:
key = (int(cpt_idx), int(sec_idx), int(sub_idx))
cpt_sec_sub_counts[key] = cpt_sec_sub_counts.get(key, 0) + 1
if sec_sub_counts:
most_common_sec_sub = max(
sec_sub_counts.items(), key=lambda x: x[1])[0]
most_common_sec, most_common_sub = most_common_sec_sub
if cpt_sec_sub_counts:
most_common_cpt_sec_sub = max(
cpt_sec_sub_counts.items(), key=lambda x: x[1])[0]
most_common_cpt, most_common_sec, most_common_sub = most_common_cpt_sec_sub
cur.execute(
"""
SELECT
d.uid, d.doc_type,
d.section_index, d.subsection_index, d.child_index,
d.section_title, d.subsection_title, d.title, d.source_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = ANY(%(sub_doc_types)s)
WHERE d.doc_type = 'subsection'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
""",
{"sec": most_common_sec, "sub": most_common_sub,
"sub_doc_types": ["subsection", "chapter"]},
{"cpt": most_common_cpt, "sec": most_common_sec, "sub": most_common_sub},
)
subsections = [_row_to_retrieved(
row, source_type="subsection") for row in cur.fetchall()]
......@@ -387,17 +420,18 @@ def retrieve(
cur.execute(
"""
SELECT
uid, doc_type,
section_index, subsection_index, child_index,
section_title, subsection_title, title, source_type,
path, markdown,
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs
WHERE doc_type = ANY(%(sec_doc_types)s)
AND section_index = %(sec)s
FROM docs d
WHERE d.doc_type = ANY(%(sec_doc_types)s)
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
""",
{"sec": most_common_sec, "sec_doc_types": [
"section", "oberchapter"]},
{"cpt": most_common_cpt, "sec": most_common_sec, "sec_doc_types": [
"section"]},
)
sections_docs = [_row_to_retrieved(
row, source_type="section") for row in cur.fetchall()]
......@@ -406,79 +440,106 @@ def retrieve(
"""
SELECT
d.uid, d.doc_type,
d.section_index, d.subsection_index, d.child_index,
d.section_title, d.subsection_title, d.title, d.source_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'chapter'
AND d.chapter_index = %(cpt)s
""",
{"cpt": most_common_cpt},
)
chapter_docs = [_row_to_retrieved(
row, source_type="chapter") for row in cur.fetchall()]
cur.execute(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
1 - (d.embedding <=> %(qvec)s) AS score
FROM docs d
WHERE d.doc_type = 'child'
AND chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
ORDER BY d.embedding <=> %(qvec)s
LIMIT 5;
LIMIT %(fill_k)s;
""",
{"qvec": qvec, "sec": most_common_sec,
"sub": most_common_sub},
{"qvec": qvec, "cpt": most_common_cpt, "sec": most_common_sec,
"sub": most_common_sub, "fill_k": max(k, 5)},
)
additional_children = [_row_to_retrieved(
row) for row in cur.fetchall()]
existing_uids = {child.uid for child in children}
existing_uids = {child.uid for child in children_direct}
new_children = [
child for child in additional_children if child.uid not in existing_uids]
children.extend(new_children)
children_expanded.extend(new_children)
needed = max(0, k - len(children_direct))
if needed:
fill_children = new_children[:needed]
children_expanded.extend(fill_children)
children = [*children_direct, *fill_children]
else:
children = list(children_direct)
if neighbor_expand and neighbor_expand > 0:
wanted: set[tuple[int, int, int]] = set()
wanted: set[tuple[int, int, int, int]] = set()
for child in children:
cpti = child.metadata.get("chapter_index")
si = child.metadata.get("section_index")
ssi = child.metadata.get("subsection_index")
ci = child.metadata.get("child_index")
if si is None or ssi is None or ci is None:
if cpti is None or si is None or ssi is None or ci is None:
continue
for dx in range(-neighbor_expand, neighbor_expand + 1):
if dx == 0:
continue
wanted.add((int(si), int(ssi), int(ci) + dx))
wanted.add((int(cpti), int(si), int(ssi), int(ci) + dx))
if wanted:
triples = sorted(wanted)
sec_arr = [a for (a, b, cidx) in triples]
sub_arr = [b for (a, b, cidx) in triples]
child_arr = [cidx for (a, b, cidx) in triples]
quadruple = sorted(wanted)
cpt_arr = [a for (a, b, c, cidx) in quadruple]
sec_arr = [b for (a, b, c, cidx) in quadruple]
sub_arr = [c for (a, b, c, cidx) in quadruple]
child_arr = [cidx for (a, b, c, cidx) in quadruple]
cur.execute(
"""
SELECT
d.uid, d.doc_type,
d.section_index, d.subsection_index, d.child_index,
d.section_title, d.subsection_title, d.title, d.source_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
0 AS score
FROM docs d
JOIN unnest(%(sec_arr)s::int[], %(sub_arr)s::int[], %(child_arr)s::int[]) AS u(sec, sub, child)
ON d.section_index = u.sec AND d.subsection_index = u.sub AND d.child_index = u.child
JOIN unnest(%(cpt_arr)s::int[],%(sec_arr)s::int[], %(sub_arr)s::int[], %(child_arr)s::int[]) AS u(cpt, sec, sub, child)
ON d.chapter_index = u.cpt AND d.section_index = u.sec AND d.subsection_index = u.sub AND d.child_index = u.child
WHERE d.doc_type = 'child'
""",
{"sec_arr": sec_arr, "sub_arr": sub_arr,
{"cpt_arr": cpt_arr, "sec_arr": sec_arr, "sub_arr": sub_arr,
"child_arr": child_arr},
)
neighbors = [_row_to_retrieved(
row) for row in cur.fetchall()]
children = [*children_direct, *children_expanded]
child_uids = {child.uid for child in children}
neighbors = [
neighbor for neighbor in neighbors if neighbor.uid not in child_uids]
retrivla_dict = {
"children_direct": children_direct,
"children_expanded": children_expanded,
"chapters": chapter_docs,
"subsections": subsections,
"sections": sections_docs,
"neighbors": neighbors,
}
sources = _retrivla_to_sources(retrivla_dict)
return sources
return sorted(sources, key=lambda source: source.score, reverse=True)
# --------------------------------------------------------------------------------------------------------------------
# Retrival mit Subsection Referenzen
......@@ -510,8 +571,8 @@ def load_children_for_subsections(
sql = """
SELECT
d.uid, d.doc_type,
d.section_index, d.subsection_index, d.child_index,
d.section_title, d.subsection_title, d.title, d.source_type,
d.chapter_index, d.section_index, d.subsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
......@@ -650,7 +711,7 @@ def _retrivla_to_sources(retrievd: Dict[str, List[Retrieved]]) -> List[Source]:
for retrival in retrieved_grouep:
sources.append(Source(
source_id=SourceID(
chapter_title="none",
chapter_title=retrival.metadata.get("chapter_title"),
section_title=retrival.metadata.get("section_title"),
subsection_title=retrival.metadata.get("subsection_title"),
title=retrival.metadata.get("title"),
......
......@@ -59,8 +59,8 @@ def retrieve(
sql = f"""
SELECT
uid, doc_type,
section_index, subsection_index, child_index,
section_title, subsection_title, title, source_type,
chapter_index, section_index, subsection_index, child_index,
chapter_title, section_title, subsection_title, title, source_type,
path, markdown,
1 - (embedding <=> %(qvec)s) AS score
FROM docs
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment