Commit c4ee9dda authored by Kantz's avatar Kantz
Browse files

anpassung des Sokrates auf neue Struktur.

parent e31b3ff9
...@@ -127,7 +127,7 @@ VITE_PROXY_TARGET="http://<BACKEND_HOST>:8000" ...@@ -127,7 +127,7 @@ VITE_PROXY_TARGET="http://<BACKEND_HOST>:8000"
Template files: Template files:
- `math-tutor/docker/docker-compose.yml` - `math-tutor/docker/docker-compose.yaml`
- `math-tutor/docker/nginx.conf` - `math-tutor/docker/nginx.conf`
- `math-tutor/backend/Dockerfile` - `math-tutor/backend/Dockerfile`
- `math-tutor/frontend/Dockerfile` - `math-tutor/frontend/Dockerfile`
...@@ -190,7 +190,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa ...@@ -190,7 +190,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
```powershell ```powershell
cd math-tutor/backend cd math-tutor/backend
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
python -m scripts.generate_socratic_chats --source-root sources/lecture_script --output sources/inital_socratic_prompt/initial_prompts.yml python -m scripts.generate_socratic_chats --source-root sources/lecture_script --output sources/inital_socratic_prompt/initial_prompts.yaml
``` ```
## Configuration ## Configuration
......
...@@ -5,12 +5,16 @@ from typing import Iterable ...@@ -5,12 +5,16 @@ from typing import Iterable
from app.deterministic_services import llm_client from app.deterministic_services import llm_client
HINT_SYSTEM_PROMPT = """ ParentRef = tuple[int, int, int, int]
SubsectionRef = tuple[int, int, int]
HINT_SYSTEM_PROMPT = r"""
Du bist ein didaktischer Mathe-Tutor. Du bist ein didaktischer Mathe-Tutor.
Antworte auf Deutsch, kurz und präzise. Antworte auf Deutsch, kurz und präzise.
Nutze ausschließlich den bereitgestellten Kontext. Nutze ausschließlich den bereitgestellten Kontext.
Wenn der Student eine Frage stellt die nichts mit dem Thema zu tun hat gehe nicht darauf ein. Wenn der Student eine Frage stellt die nichts mit dem Thema zu tun hat gehe nicht darauf ein.
Stelle offene Verständnisfragen und leite den Studeten sokratisch. Stelle offene Verständnisfragen und leite den Studeten sokratisch.
Nutze keine Lückentexte oder Frage auf die man mit einem Wort antworten kann. Nutze keine Lückentexte oder Frage auf die man mit einem Wort antworten kann.
Fordere den Nutzer auf mit einem ganzen Satz oder einer Formel zu antworten, falls er wiederholt nur 1 Wort eingaben macht. Fordere den Nutzer auf mit einem ganzen Satz oder einer Formel zu antworten, falls er wiederholt nur 1 Wort eingaben macht.
...@@ -25,21 +29,33 @@ minf steht für minus unendlich. ...@@ -25,21 +29,33 @@ minf steht für minus unendlich.
""" """
def _format_subsection_refs(subsection_refs: Iterable[tuple[int, int, int]] | None) -> str: def _format_parent_refs(parent_refs: Iterable[ParentRef] | None) -> str:
if not subsection_refs: if not parent_refs:
return "Keine Subsection-Referenzen übergeben." return "Keine Parent-Referenzen übergeben."
formatted = [f"{chap}:{sec}:{sub}" for chap, sec, sub in subsection_refs] formatted: list[str] = []
for chap, sec, sub, subsub in parent_refs:
if sub <= 0:
formatted.append(f"{chap}:{sec}")
elif subsub <= 0:
formatted.append(f"{chap}:{sec}:{sub}")
else:
formatted.append(f"{chap}:{sec}:{sub}:{subsub}")
return ", ".join(formatted) return ", ".join(formatted)
def generate_dialog( def generate_dialog(
query: str | None, query: str | None,
subsection_refs: list[tuple[int, int, int]] | None = None, subsection_refs: list[SubsectionRef] | None = None,
parent_refs: list[ParentRef] | None = None,
history: list[dict] | None = None, history: list[dict] | None = None,
sources: str | None = None, sources: str | None = None,
) -> str: ) -> str:
effective_parent_refs = parent_refs
if effective_parent_refs is None and subsection_refs is not None:
effective_parent_refs = [(chap, sec, sub, 0) for chap, sec, sub in subsection_refs]
context_parts = [ context_parts = [
f"Subsection-Referenzen:\n{_format_subsection_refs(subsection_refs)}", f"Parent-Referenzen:\n{_format_parent_refs(effective_parent_refs)}",
] ]
if sources: if sources:
context_parts.append(f"Kontext/Sources:\n{sources}") context_parts.append(f"Kontext/Sources:\n{sources}")
......
...@@ -195,10 +195,10 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe ...@@ -195,10 +195,10 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
if not task_catalog.select_subsection_by_key(sheet, request.subsection_key): if not task_catalog.select_subsection_by_key(sheet, request.subsection_key):
raise HTTPException(status_code=404, detail="subsection not found") raise HTTPException(status_code=404, detail="subsection not found")
refs = task_catalog.get_selected_subsection_refs(sheet) refs = task_catalog.get_selected_subsection_parent_refs(sheet)
sources = retrieval_store.retrieve_for_subsections( sources = retrieval_store.retrieve_for_parent_refs(
pg_url=config.get_postgres_url(), pg_url=config.get_postgres_url(),
subsection_refs=refs, parent_refs=refs,
) )
context_store.set_sources(sheet, sources) context_store.set_sources(sheet, sources)
context_store.set_initialized(sheet, True) context_store.set_initialized(sheet, True)
......
...@@ -36,6 +36,7 @@ class TopicEntry(BaseModel): ...@@ -36,6 +36,7 @@ class TopicEntry(BaseModel):
class SubsectionEntry(BaseModel): class SubsectionEntry(BaseModel):
subsection_key: str subsection_key: str
label: str label: str
level: str = ""
refs: List[List[int]] refs: List[List[int]]
summary: str = "" summary: str = ""
......
...@@ -21,28 +21,27 @@ def _apply_selected_subsection( ...@@ -21,28 +21,27 @@ def _apply_selected_subsection(
def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) -> int: def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) -> int:
# Deprecated: socratic retrieval still uses subsection refs until it is migrated to the new parent model. refs = task_catalog.get_selected_subsection_parent_refs(state.sheet)
refs = task_catalog.get_selected_subsection_refs(state.sheet)
if not refs: if not refs:
return 0 return 0
def _retrieve() -> dict: def _retrieve() -> dict:
sources = retrieval_store.retrieve_for_subsections( sources = retrieval_store.retrieve_for_parent_refs(
pg_url=config.get_postgres_url(), pg_url=config.get_postgres_url(),
subsection_refs=refs, parent_refs=refs,
) )
context_store.update_retrieval_context(state.sheet, sources) context_store.update_retrieval_context(state.sheet, sources)
return { return {
"subsection_refs": refs, "parent_refs": refs,
"source_count": len(sources), "source_count": len(sources),
} }
result = base.log_timed_call( result = base.log_timed_call(
state.tool_log, state.tool_log,
"retrieve_context_with_socratic_subsections", "retrieve_context_with_socratic_topics",
{ {
"query": query_text, "query": query_text,
"subsection_refs": refs, "parent_refs": refs,
}, },
_retrieve, _retrieve,
) )
...@@ -54,16 +53,16 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None: ...@@ -54,16 +53,16 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
def _on_turn_logic(state: base.ChatState) -> None: def _on_turn_logic(state: base.ChatState) -> None:
if not task_catalog.get_selected_subsection_refs(state.sheet): if not task_catalog.get_selected_subsection_parent_refs(state.sheet):
return return
def _on_build_reply(state: base.ChatState) -> str | None: def _on_build_reply(state: base.ChatState) -> str | None:
history_turns = context_store.get_history_turns(state.sheet) history_turns = context_store.get_history_turns(state.sheet)
subsection_refs = task_catalog.get_selected_subsection_refs(state.sheet) parent_refs = task_catalog.get_selected_subsection_parent_refs(state.sheet)
args = { args = {
"query": state.last_user, "query": state.last_user,
"subsection_refs": subsection_refs, "parent_refs": parent_refs,
"history": history_turns, "history": history_turns,
"sources": "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)]), "sources": "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)]),
} }
......
...@@ -9,7 +9,7 @@ import yaml ...@@ -9,7 +9,7 @@ import yaml
from app.deterministic_services import task_catalog from app.deterministic_services import task_catalog
PROMPTS_PATH = Path(__file__).resolve().parents[2] / "sources" / "inital_socratic_prompt" / "initial_prompts.yml" PROMPTS_PATH = Path(__file__).resolve().parents[2] / "sources" / "inital_socratic_prompt" / "initial_prompts.yaml"
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
...@@ -28,7 +28,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str, ...@@ -28,7 +28,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
items: dict[str, dict[str, Any]] = {} items: dict[str, dict[str, Any]] = {}
for raw_key, raw_value in raw_items.items(): for raw_key, raw_value in raw_items.items():
key = task_catalog._normalize_subsection_key(str(raw_key)) key = task_catalog._normalize_parent_ref_key(str(raw_key))
if not key or not isinstance(raw_value, dict): if not key or not isinstance(raw_value, dict):
continue continue
items[key] = raw_value items[key] = raw_value
...@@ -36,7 +36,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str, ...@@ -36,7 +36,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str: def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
normalized_key = task_catalog._normalize_subsection_key(subsection_key) normalized_key = task_catalog._normalize_parent_ref_key(subsection_key)
if not normalized_key: if not normalized_key:
raise ValueError("invalid subsection key") raise ValueError("invalid subsection key")
...@@ -53,31 +53,41 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str: ...@@ -53,31 +53,41 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
def build_subsection_catalog(path: Path = PROMPTS_PATH) -> list[dict[str, Any]]: def build_subsection_catalog(path: Path = PROMPTS_PATH) -> list[dict[str, Any]]:
items = load_initial_prompt_items(path) items = load_initial_prompt_items(path)
subsection_summaries = task_catalog.load_subsection_summaries() topic_summaries = task_catalog.load_topic_summaries()
response: list[dict[str, Any]] = [] response: list[dict[str, Any]] = []
for raw_key, item in items.items(): for raw_key, item in items.items():
subsection_key = task_catalog._normalize_subsection_key(str(raw_key)) subsection_key = task_catalog._normalize_parent_ref_key(str(raw_key))
if not subsection_key: if not subsection_key:
continue continue
label_source = str(item.get("subsection") or subsection_key) label_source = str(item.get("label") or item.get("subsection") or subsection_key)
label = task_catalog._format_subsection_label(label_source) label = task_catalog._format_subsection_label(label_source)
ref = task_catalog._parse_subsection_ref(str(item.get("index") or "")) refs: list[list[int]] = []
refs = [[int(ref[0]), int(ref[1]), int(ref[2])]] if ref is not None else [] refs_raw = item.get("refs", [])
if isinstance(refs_raw, list):
for raw_ref in refs_raw:
parsed = task_catalog._parse_parent_ref(raw_ref)
if parsed is not None:
refs.append(task_catalog._parent_ref_to_list(parsed))
if not refs:
parsed_index = task_catalog._parse_parent_ref(str(item.get("index") or ""))
if parsed_index is not None:
refs.append(task_catalog._parent_ref_to_list(parsed_index))
response.append( response.append(
{ {
"subsection_key": subsection_key, "subsection_key": subsection_key,
"label": label or task_catalog._format_subsection_label(subsection_key), "label": label or task_catalog._format_subsection_label(subsection_key),
"level": str(item.get("level") or (task_catalog._parent_ref_level(tuple(refs[0])) if refs else "")).strip(),
"refs": refs, "refs": refs,
"summary": subsection_summaries.get(subsection_key, ""), "summary": topic_summaries.get(subsection_key, ""),
} }
) )
response.sort( response.sort(
key=lambda item: ( key=lambda item: (
item["refs"][0] if item["refs"] else (9999, 9999, 9999), item["refs"][0] if item["refs"] else (9999, 9999, 9999, 9999),
item["subsection_key"], item["subsection_key"],
) )
) )
......
...@@ -172,6 +172,12 @@ def _extract_subsection_summary(text: str) -> str: ...@@ -172,6 +172,12 @@ def _extract_subsection_summary(text: str) -> str:
def load_topic_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]: def load_topic_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
summaries: dict[str, str] = {} summaries: dict[str, str] = {}
folders = ( folders = (
base_dir / "lecture_script" / "sections",
base_dir / "lecture_script" / "subsections",
base_dir / "lecture_script" / "subsubsections",
base_dir / "stochastik_context" / "sections",
base_dir / "stochastik_context" / "subsections",
base_dir / "stochastik_context" / "subsubsections",
base_dir / "lecture_script_new" / "sections", base_dir / "lecture_script_new" / "sections",
base_dir / "lecture_script_new" / "subsections", base_dir / "lecture_script_new" / "subsections",
base_dir / "lecture_script_new" / "subsubsections", base_dir / "lecture_script_new" / "subsubsections",
...@@ -343,14 +349,15 @@ def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str, ...@@ -343,14 +349,15 @@ def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str,
response: list[dict[str, Any]] = [] response: list[dict[str, Any]] = []
for item in build_topic_catalog(path): for item in build_topic_catalog(path):
refs = item.get("refs", []) refs = item.get("refs", [])
if item.get("level") != "subsection" or not refs: if not refs:
continue continue
ref = refs[0] ref = refs[0]
response.append( response.append(
{ {
"subsection_key": item["topic_key"], "subsection_key": item["topic_key"],
"label": item["label"], "label": item["label"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2])]], "level": item["level"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2]), int(ref[3])]],
"summary": item["summary"], "summary": item["summary"],
} }
) )
...@@ -371,14 +378,15 @@ def _resolve_task_subsection_options( ...@@ -371,14 +378,15 @@ def _resolve_task_subsection_options(
response: list[dict[str, Any]] = [] response: list[dict[str, Any]] = []
for option in options: for option in options:
refs = option.get("refs", []) refs = option.get("refs", [])
if option.get("level") != "subsection" or not refs: if not refs:
continue continue
ref = refs[0] ref = refs[0]
response.append( response.append(
{ {
"subsection_key": option["topic_key"], "subsection_key": option["topic_key"],
"label": option["label"], "label": option["label"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2])]], "level": option["level"],
"refs": [[int(ref[0]), int(ref[1]), int(ref[2]), int(ref[3])]],
"summary": option.get("summary", ""), "summary": option.get("summary", ""),
} }
) )
...@@ -591,6 +599,7 @@ def set_selected_task( ...@@ -591,6 +599,7 @@ def set_selected_task(
sheet.pop("task_subsection_refs", None) sheet.pop("task_subsection_refs", None)
sheet.pop("selected_subsection_key", None) sheet.pop("selected_subsection_key", None)
sheet.pop("selected_subsection_label", None) sheet.pop("selected_subsection_label", None)
sheet.pop("selected_subsection_parent_refs", None)
sheet.pop("selected_subsection_refs", None) sheet.pop("selected_subsection_refs", None)
...@@ -600,21 +609,27 @@ def set_selected_subsection( ...@@ -600,21 +609,27 @@ def set_selected_subsection(
subsection_option: dict[str, Any], subsection_option: dict[str, Any],
) -> None: ) -> None:
refs_raw = subsection_option.get("refs", []) refs_raw = subsection_option.get("refs", [])
refs: list[tuple[int, int, int]] = [] refs: list[ParentRef] = []
if isinstance(refs_raw, list): if isinstance(refs_raw, list):
for item in refs_raw: for item in refs_raw:
if isinstance(item, (list, tuple)) and len(item) >= 3: parsed = _parse_parent_ref(item)
try: if parsed is not None:
refs.append((int(item[0]), int(item[1]), int(item[2]))) refs.append(parsed)
except Exception:
continue
if not refs: if not refs:
return return
sheet["selected_subsection_key"] = str(subsection_option.get("subsection_key", "")).strip() key = str(subsection_option.get("subsection_key") or subsection_option.get("topic_key") or "").strip()
sheet["selected_subsection_key"] = key
sheet["selected_subsection_label"] = str(subsection_option.get("label", "")).strip() sheet["selected_subsection_label"] = str(subsection_option.get("label", "")).strip()
sheet["selected_subsection_refs"] = [[chap, sec, sub] for chap, sec, sub in sorted({*refs})] parent_refs = sorted({_normalize_parent_ref(ref) for ref in refs})
sheet["selected_subsection_parent_refs"] = [_parent_ref_to_list(ref) for ref in parent_refs]
sheet["selected_subsection_refs"] = [
[chap, sec, sub]
for chap, sec, sub, subsub in parent_refs
if sub > 0 and subsub <= 0
]
sheet.pop("task_file_id", None)
sheet.pop("task_id", None) sheet.pop("task_id", None)
sheet.pop("task_parent_refs", None) sheet.pop("task_parent_refs", None)
sheet.pop("task_subsection_refs", None) sheet.pop("task_subsection_refs", None)
...@@ -651,8 +666,7 @@ def select_subsection_by_ids( ...@@ -651,8 +666,7 @@ def select_subsection_by_ids(
if not task_file: if not task_file:
return False return False
subsection_map = load_subsection_map() options = _resolve_task_subsection_options(task_file)
options = _resolve_task_subsection_options(task_file, subsection_map=subsection_map)
normalized_key = _normalize_subsection_key(subsection_key) normalized_key = _normalize_subsection_key(subsection_key)
if not normalized_key: if not normalized_key:
return False return False
...@@ -669,7 +683,11 @@ def select_subsection_by_key( ...@@ -669,7 +683,11 @@ def select_subsection_by_key(
subsection_key: str, subsection_key: str,
subsection_map: dict[str, tuple[int, int, int]] | None = None, subsection_map: dict[str, tuple[int, int, int]] | None = None,
) -> bool: ) -> bool:
mapping = subsection_map if subsection_map is not None else load_subsection_map() mapping: dict[str, ParentRef]
if subsection_map is None:
mapping = load_topic_map()
else:
mapping = {key: (int(ref[0]), int(ref[1]), int(ref[2]), 0) for key, ref in subsection_map.items()}
normalized_key = _normalize_subsection_key(subsection_key) normalized_key = _normalize_subsection_key(subsection_key)
if not normalized_key: if not normalized_key:
return False return False
...@@ -679,7 +697,13 @@ def select_subsection_by_key( ...@@ -679,7 +697,13 @@ def select_subsection_by_key(
sheet["selected_subsection_key"] = normalized_key sheet["selected_subsection_key"] = normalized_key
sheet["selected_subsection_label"] = _format_subsection_label(normalized_key) sheet["selected_subsection_label"] = _format_subsection_label(normalized_key)
sheet["selected_subsection_refs"] = [[int(ref[0]), int(ref[1]), int(ref[2])]] normalized_ref = _normalize_parent_ref(ref)
sheet["selected_subsection_parent_refs"] = [_parent_ref_to_list(normalized_ref)]
chap, sec, sub, subsub = normalized_ref
if sub > 0 and subsub <= 0:
sheet["selected_subsection_refs"] = [[chap, sec, sub]]
else:
sheet["selected_subsection_refs"] = []
sheet.pop("task_file_id", None) sheet.pop("task_file_id", None)
sheet.pop("task_id", None) sheet.pop("task_id", None)
sheet.pop("task_parent_refs", None) sheet.pop("task_parent_refs", None)
...@@ -702,17 +726,24 @@ def get_selected_subsection_ids(sheet: dict[str, Any]) -> tuple[str | None, str ...@@ -702,17 +726,24 @@ def get_selected_subsection_ids(sheet: dict[str, Any]) -> tuple[str | None, str
def get_selected_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int, int]]: def get_selected_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int, int]]:
refs_raw = sheet.get("selected_subsection_refs", []) refs: list[tuple[int, int, int]] = []
for chap, sec, sub, subsub in get_selected_subsection_parent_refs(sheet):
if sub <= 0 or subsub > 0:
continue
refs.append((chap, sec, sub))
return sorted(set(refs))
def get_selected_subsection_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]:
refs_raw = sheet.get("selected_subsection_parent_refs")
if not isinstance(refs_raw, list): if not isinstance(refs_raw, list):
return [] refs_raw = sheet.get("selected_subsection_refs", [])
refs: set[tuple[int, int, int]] = set() refs: set[ParentRef] = set()
for item in refs_raw: for item in refs_raw:
if isinstance(item, (list, tuple)) and len(item) >= 3: parsed = _parse_parent_ref(item)
try: if parsed is not None:
refs.add((int(item[0]), int(item[1]), int(item[2]))) refs.add(_normalize_parent_ref(parsed))
except Exception:
continue
return sorted(refs) return sorted(refs)
......
...@@ -14,17 +14,17 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat ...@@ -14,17 +14,17 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat
BACKEND_ROOT = Path(__file__).resolve().parents[2] BACKEND_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_SOURCE_ROOT = BACKEND_ROOT / "sources" / "with_chapters" DEFAULT_SOURCE_ROOT = BACKEND_ROOT / "sources" / "lecture_script"
DEFAULT_OUTPUT = BACKEND_ROOT / "sources" / "inital_socratic_prompt" / "initial_prompts.yml" DEFAULT_OUTPUT = BACKEND_ROOT / "sources" / "inital_socratic_prompt" / "initial_prompts.yaml"
DEFAULT_TEMPLATE = BACKEND_ROOT / "sources" / "inital_socratic_prompt" / "template.yml" DEFAULT_TEMPLATE = BACKEND_ROOT / "sources" / "inital_socratic_prompt" / "template.yaml"
DEFAULT_PROMPT = ( DEFAULT_PROMPT = (
"Was sind die Themen dieses Abschnitts? Numeriere sie durch. Frage mich, mit welchem ich mich " "Was sind die Themen dieses Abschnitts? Numeriere sie durch. Frage mich, mit welchem ich mich "
"zuerst beschäftigen möchte." "zuerst beschäftigen möchte."
) )
DEFAULT_TEMPLATE_FIELDS = ["subsection", "index", "inital_message", "sources"] DEFAULT_TEMPLATE_FIELDS = ["label", "level", "index", "refs", "inital_message", "sources"]
SubsectionRef = tuple[int, int, int] ParentRef = tuple[int, int, int, int]
@dataclass(frozen=True) @dataclass(frozen=True)
...@@ -33,8 +33,9 @@ class MarkdownDoc: ...@@ -33,8 +33,9 @@ class MarkdownDoc:
relative_path: str relative_path: str
meta: dict[str, Any] meta: dict[str, Any]
body: str body: str
ref: SubsectionRef | None ref: ParentRef | None
title: str title: str
level: str
def _meta_int(meta: dict[str, Any], key: str) -> int | None: def _meta_int(meta: dict[str, Any], key: str) -> int | None:
...@@ -47,87 +48,119 @@ def _meta_int(meta: dict[str, Any], key: str) -> int | None: ...@@ -47,87 +48,119 @@ def _meta_int(meta: dict[str, Any], key: str) -> int | None:
return None return None
def _detect_doc_level(path: Path) -> str:
folder = path.parent.name.lower()
if folder == "sections":
return "section"
if folder == "subsections":
return "subsection"
if folder == "subsubsections":
return "subsubsection"
return "child"
def _resolve_parent_ref(meta: dict[str, Any], title: str, topic_map: dict[str, ParentRef]) -> ParentRef | None:
chapter_index = _meta_int(meta, "chapter_index")
section_index = _meta_int(meta, "section_index")
if chapter_index is not None and section_index is not None:
subsection_index = _meta_int(meta, "subsection_index") or 0
subsubsection_index = _meta_int(meta, "subsubsection_index") or 0
return task_catalog._normalize_parent_ref(
(chapter_index, section_index, subsection_index, subsubsection_index)
)
key_candidates = [
title,
str(meta.get("subsubsection_title") or ""),
str(meta.get("subsection_title") or ""),
str(meta.get("section_title") or ""),
]
for candidate in key_candidates:
key = task_catalog._normalize_parent_ref_key(candidate)
if key and key in topic_map:
return topic_map[key]
return None
def _read_markdown_doc( def _read_markdown_doc(
path: Path, path: Path,
source_root: Path, source_root: Path,
subsection_map: dict[str, SubsectionRef], topic_map: dict[str, ParentRef],
) -> MarkdownDoc: ) -> MarkdownDoc:
text = path.read_text(encoding="utf-8") text = path.read_text(encoding="utf-8")
meta, body = parse_markdown_with_frontmatter(text) meta, body = parse_markdown_with_frontmatter(text)
title = str( title = str(
meta.get("title") meta.get("title")
or meta.get("subsubsection_title")
or meta.get("subsection_title") or meta.get("subsection_title")
or meta.get("section_title") or meta.get("section_title")
or path.stem or path.stem
).strip() ).strip()
ref = _resolve_subsection_ref(meta, title, subsection_map)
return MarkdownDoc( return MarkdownDoc(
path=path, path=path,
relative_path=path.relative_to(source_root).as_posix(), relative_path=path.relative_to(source_root).as_posix(),
meta=meta, meta=meta,
body=body.strip(), body=body.strip(),
ref=ref, ref=_resolve_parent_ref(meta, title, topic_map),
title=title, title=title,
level=_detect_doc_level(path),
) )
def _resolve_subsection_ref(
meta: dict[str, Any],
title: str,
subsection_map: dict[str, SubsectionRef],
) -> SubsectionRef | None:
chapter_index = _meta_int(meta, "chapter_index")
section_index = _meta_int(meta, "section_index")
subsection_index = _meta_int(meta, "subsection_index")
if chapter_index is not None and section_index is not None and subsection_index is not None:
return (chapter_index, section_index, subsection_index)
key_candidates = [
title,
str(meta.get("subsection_title") or ""),
str(meta.get("title") or ""),
]
for candidate in key_candidates:
key = task_catalog._normalize_subsection_key(candidate)
if key and key in subsection_map:
return subsection_map[key]
if section_index is not None and subsection_index is not None:
matches = [
ref
for ref in subsection_map.values()
if int(ref[1]) == section_index and int(ref[2]) == subsection_index
]
unique_matches = sorted(set(matches))
if len(unique_matches) == 1:
return unique_matches[0]
return None
def _load_markdown_docs( def _load_markdown_docs(
source_root: Path, source_root: Path,
folder_name: str, folder_name: str,
subsection_map: dict[str, SubsectionRef], topic_map: dict[str, ParentRef],
) -> list[MarkdownDoc]: ) -> list[MarkdownDoc]:
folder = source_root / folder_name folder = source_root / folder_name
if not folder.exists(): if not folder.exists():
return [] return []
return [ return [
_read_markdown_doc(path, source_root, subsection_map) _read_markdown_doc(path, source_root, topic_map)
for path in sorted(folder.glob("*.md")) for path in sorted(folder.glob("*.md"))
] ]
def load_subsections_and_children( def _is_descendant_ref(candidate: ParentRef, ancestor: ParentRef) -> bool:
cand = task_catalog._normalize_parent_ref(candidate)
anc = task_catalog._normalize_parent_ref(ancestor)
if cand == anc:
return False
if anc[2] <= 0:
return cand[0] == anc[0] and cand[1] == anc[1]
if anc[3] <= 0:
return cand[0] == anc[0] and cand[1] == anc[1] and cand[2] == anc[2]
return False
def _leaf_topic_map(topic_map: dict[str, ParentRef]) -> dict[str, ParentRef]:
refs = list(topic_map.values())
leaves: dict[str, ParentRef] = {}
for key, ref in topic_map.items():
if any(_is_descendant_ref(other, ref) for other in refs):
continue
leaves[key] = task_catalog._normalize_parent_ref(ref)
return leaves
def load_topics_and_children(
source_root: Path, source_root: Path,
subsection_map: dict[str, SubsectionRef] | None = None, topic_map: dict[str, ParentRef] | None = None,
) -> tuple[list[MarkdownDoc], dict[SubsectionRef, list[MarkdownDoc]]]: ) -> tuple[dict[ParentRef, MarkdownDoc], dict[ParentRef, list[MarkdownDoc]]]:
mapping = subsection_map if subsection_map is not None else task_catalog.load_subsection_map() mapping = topic_map if topic_map is not None else task_catalog.load_topic_map()
subsections = _load_markdown_docs(source_root, "subsections", mapping) parents: dict[ParentRef, MarkdownDoc] = {}
children = _load_markdown_docs(source_root, "childs", mapping) parent_docs = [
*_load_markdown_docs(source_root, "sections", mapping),
*_load_markdown_docs(source_root, "subsections", mapping),
*_load_markdown_docs(source_root, "subsubsections", mapping),
]
for doc in parent_docs:
if doc.ref is None:
continue
parents.setdefault(doc.ref, doc)
children_by_ref: dict[SubsectionRef, list[MarkdownDoc]] = {} children = _load_markdown_docs(source_root, "childs", mapping)
children_by_ref: dict[ParentRef, list[MarkdownDoc]] = {}
for child in children: for child in children:
if child.ref is None: if child.ref is None:
continue continue
...@@ -136,25 +169,27 @@ def load_subsections_and_children( ...@@ -136,25 +169,27 @@ def load_subsections_and_children(
for ref_children in children_by_ref.values(): for ref_children in children_by_ref.values():
ref_children.sort(key=lambda item: (_meta_int(item.meta, "child_index") or 0, item.relative_path)) ref_children.sort(key=lambda item: (_meta_int(item.meta, "child_index") or 0, item.relative_path))
return subsections, children_by_ref return parents, children_by_ref
def _subsection_key(subsection: MarkdownDoc) -> str:
return task_catalog._normalize_subsection_key(subsection.title or subsection.path.stem)
def _format_ref(ref: ParentRef) -> str:
chap, sec, sub, subsub = task_catalog._normalize_parent_ref(ref)
if sub <= 0:
return f"{chap}-{sec}"
if subsub <= 0:
return f"{chap}-{sec}-{sub}"
return f"{chap}-{sec}-{sub}-{subsub}"
def _format_ref(ref: SubsectionRef) -> str:
return f"{ref[0]}-{ref[1]}-{ref[2]}"
def build_llm_sources_text(topic: MarkdownDoc, children: list[MarkdownDoc]) -> str:
def build_llm_sources_text(subsection: MarkdownDoc, children: list[MarkdownDoc]) -> str: ref_text = _format_ref(topic.ref) if topic.ref else ""
ref_text = _format_ref(subsection.ref) if subsection.ref else ""
parts = [ parts = [
f"Abschnitt: {subsection.title}", f"Thema: {topic.title}",
f"Level: {topic.level}",
f"Index: {ref_text}", f"Index: {ref_text}",
f"Datei: {subsection.relative_path}", f"Datei: {topic.relative_path}",
"Abschnittsinhalt:", "Themeninhalt:",
subsection.body, topic.body,
] ]
for child in children: for child in children:
...@@ -171,12 +206,12 @@ def build_llm_sources_text(subsection: MarkdownDoc, children: list[MarkdownDoc]) ...@@ -171,12 +206,12 @@ def build_llm_sources_text(subsection: MarkdownDoc, children: list[MarkdownDoc])
return "\n".join(parts).strip() return "\n".join(parts).strip()
def generate_initial_message(subsection: MarkdownDoc, children: list[MarkdownDoc]) -> str: def generate_initial_message(topic: MarkdownDoc, children: list[MarkdownDoc]) -> str:
sources_text = build_llm_sources_text(subsection, children) sources_text = build_llm_sources_text(topic, children)
refs = [subsection.ref] if subsection.ref else None refs = [topic.ref] if topic.ref else None
return socratic_LLM.generate_dialog( return socratic_LLM.generate_dialog(
query=DEFAULT_PROMPT, query=DEFAULT_PROMPT,
subsection_refs=refs, parent_refs=refs,
history=None, history=None,
sources=sources_text, sources=sources_text,
).strip() ).strip()
...@@ -240,43 +275,46 @@ def build_initial_prompt_index( ...@@ -240,43 +275,46 @@ def build_initial_prompt_index(
output_path = output_path.resolve() output_path = output_path.resolve()
template_fields = _load_template_fields(template_path) template_fields = _load_template_fields(template_path)
existing_items = _load_existing_items(output_path) existing_items = _load_existing_items(output_path)
subsections, children_by_ref = load_subsections_and_children(source_root) topic_map = task_catalog.load_topic_map()
leaf_topics = _leaf_topic_map(topic_map)
parents_by_ref, children_by_ref = load_topics_and_children(source_root, topic_map)
items: dict[str, dict[str, Any]] = {} items: dict[str, dict[str, Any]] = {}
warnings: list[str] = [] warnings: list[str] = []
processed = 0 processed = 0
for subsection in subsections: for key, ref in sorted(leaf_topics.items(), key=lambda item: item[1]):
if limit is not None and processed >= limit: if limit is not None and processed >= limit:
break break
key = _subsection_key(subsection) topic = parents_by_ref.get(ref)
if not key: if topic is None:
warnings.append(f"Skipping subsection without key: {subsection.relative_path}") message = f"Missing parent markdown doc for topic '{key}' ({_format_ref(ref)})"
continue if strict:
if subsection.ref is None: raise ValueError(message)
warnings.append(f"Skipping subsection without resolved index: {subsection.relative_path}") warnings.append(message)
continue continue
children = children_by_ref.get(subsection.ref, []) children = children_by_ref.get(ref, [])
if not children: if not children:
message = f"No child chunks matched subsection {subsection.relative_path}" message = f"No child chunks matched topic {topic.relative_path}"
if strict: if strict:
raise ValueError(message) raise ValueError(message)
warnings.append(message) warnings.append(message)
sources = [subsection.relative_path, *[child.relative_path for child in children]]
existing_message = str(existing_items.get(key, {}).get("inital_message") or "").strip() existing_message = str(existing_items.get(key, {}).get("inital_message") or "").strip()
if existing_message and not force: if existing_message and not force:
initial_message = existing_message initial_message = existing_message
else: else:
initial_message = generate_initial_message(subsection, children) initial_message = generate_initial_message(topic, children)
item = { item = {
"subsection": subsection.title, "label": topic.title,
"index": _format_ref(subsection.ref), "level": task_catalog._parent_ref_level(ref),
"index": _format_ref(ref),
"refs": [task_catalog._parent_ref_to_list(ref)],
"inital_message": initial_message, "inital_message": initial_message,
"sources": sources, "sources": [topic.relative_path, *[child.relative_path for child in children]],
} }
items[key] = _ordered_item(item, template_fields) items[key] = _ordered_item(item, template_fields)
processed += 1 processed += 1
...@@ -325,7 +363,7 @@ def generate_file( ...@@ -325,7 +363,7 @@ def generate_file(
def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Generate pregenerated Socratic initial prompts for subsections." description="Generate pregenerated Socratic initial prompts for leaf topics."
) )
parser.add_argument("--source-root", type=Path, default=DEFAULT_SOURCE_ROOT) parser.add_argument("--source-root", type=Path, default=DEFAULT_SOURCE_ROOT)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
......
from __future__ import annotations
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("OPENAI_BASE_URL", "http://localhost:9999")
os.environ.setdefault("OPENAI_API_KEY", "test-key")
BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
from scripts import generate_socratic_chats
def _write_markdown(path: Path, frontmatter: str, body: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"---\n{frontmatter}\n---\n{body}\n", encoding="utf-8")
class GenerateSocraticChatsTest(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = Path(tempfile.mkdtemp(prefix="socratic-prompts-"))
self.source_root = self.temp_dir / "lecture_script"
self.output_path = self.temp_dir / "initial_prompts.yaml"
self.template_path = self.temp_dir / "template.yaml"
self.template_path.write_text(
"label: ''\nlevel: ''\nindex: ''\nrefs:\n- [0, 0, 0, 0]\ninital_message: ''\nsources:\n- ''\n",
encoding="utf-8",
)
def tearDown(self) -> None:
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_build_initial_prompt_index_uses_lowest_available_leaf_level(self) -> None:
_write_markdown(
self.source_root / "sections/section-leaf.md",
'chapter_index: 1\nsection_index: 1\ntitle: "Mengen"',
"Section leaf body",
)
_write_markdown(
self.source_root / "subsections/subsection-leaf.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 3\ntitle: "Potenz- und Wurzelrechnung"',
"Subsection leaf body",
)
_write_markdown(
self.source_root / "subsections/subsection-parent.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 2\ntitle: "Grundrechenarten bei reellen Zahlen"',
"Subsection parent body",
)
_write_markdown(
self.source_root / "subsubsections/subsubsection-leaf.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 2\nsubsubsection_index: 1\ntitle: "Klammerrechnung"',
"Subsubsection leaf body",
)
_write_markdown(
self.source_root / "childs/section-child.md",
'chapter_index: 1\nsection_index: 1\nsubsection_index: 0\nsubsubsection_index: 0\nchild_index: 1\ntitle: "Mengen Child"',
"Section child body",
)
_write_markdown(
self.source_root / "childs/subsection-child.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 3\nsubsubsection_index: 0\nchild_index: 1\ntitle: "Potenz Child"',
"Subsection child body",
)
_write_markdown(
self.source_root / "childs/subsubsection-child.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 2\nsubsubsection_index: 1\nchild_index: 1\ntitle: "Klammer Child"',
"Subsubsection child body",
)
_write_markdown(
self.source_root / "childs/subsection-parent-child.md",
'chapter_index: 1\nsection_index: 2\nsubsection_index: 2\nsubsubsection_index: 0\nchild_index: 1\ntitle: "Grundrechenarten Child"',
"Subsection parent child body",
)
topic_map = {
"mengen": (1, 1, 0, 0),
"grundrechenarten bei reellen zahlen": (1, 2, 2, 0),
"klammerrechnung": (1, 2, 2, 1),
"potenz und wurzelrechnung": (1, 2, 3, 0),
}
with patch(
"scripts.generate_socratic_chats.task_catalog.load_topic_map",
return_value=topic_map,
), patch(
"scripts.generate_socratic_chats.generate_initial_message",
side_effect=lambda topic, children: f"{topic.title}|{len(children)}",
):
payload = generate_socratic_chats.build_initial_prompt_index(
source_root=self.source_root,
output_path=self.output_path,
template_path=self.template_path,
)
items = payload["items"]
self.assertEqual(sorted(items.keys()), ["klammerrechnung", "mengen", "potenz und wurzelrechnung"])
self.assertEqual(items["mengen"]["level"], "section")
self.assertEqual(items["mengen"]["index"], "1-1")
self.assertEqual(items["mengen"]["refs"], [[1, 1, 0, 0]])
self.assertEqual(items["mengen"]["inital_message"], "Mengen|1")
self.assertEqual(items["potenz und wurzelrechnung"]["level"], "subsection")
self.assertEqual(items["potenz und wurzelrechnung"]["index"], "1-2-3")
self.assertEqual(items["potenz und wurzelrechnung"]["refs"], [[1, 2, 3, 0]])
self.assertEqual(items["potenz und wurzelrechnung"]["inital_message"], "Potenz- und Wurzelrechnung|1")
self.assertEqual(items["klammerrechnung"]["level"], "subsubsection")
self.assertEqual(items["klammerrechnung"]["index"], "1-2-2-1")
self.assertEqual(items["klammerrechnung"]["refs"], [[1, 2, 2, 1]])
self.assertEqual(items["klammerrechnung"]["inital_message"], "Klammerrechnung|1")
self.assertNotIn("grundrechenarten bei reellen zahlen", items)
def test_existing_message_reuse_matches_leaf_key(self) -> None:
_write_markdown(
self.source_root / "sections/section-leaf.md",
'chapter_index: 1\nsection_index: 1\ntitle: "Mengen"',
"Section leaf body",
)
_write_markdown(
self.source_root / "childs/section-child.md",
'chapter_index: 1\nsection_index: 1\nsubsection_index: 0\nsubsubsection_index: 0\nchild_index: 1\ntitle: "Mengen Child"',
"Section child body",
)
self.output_path.write_text(
"items:\n mengen:\n inital_message: Bereits vorhanden\n",
encoding="utf-8",
)
with patch(
"scripts.generate_socratic_chats.task_catalog.load_topic_map",
return_value={"mengen": (1, 1, 0, 0)},
), patch(
"scripts.generate_socratic_chats.generate_initial_message",
side_effect=AssertionError("should not regenerate"),
):
payload = generate_socratic_chats.build_initial_prompt_index(
source_root=self.source_root,
output_path=self.output_path,
template_path=self.template_path,
)
self.assertEqual(payload["items"]["mengen"]["inital_message"], "Bereits vorhanden")
if __name__ == "__main__":
unittest.main()
...@@ -120,16 +120,18 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -120,16 +120,18 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items", "app.deterministic_services.socratic_oranisator.load_initial_prompt_items",
return_value={ return_value={
"quadratische gleichungen": { "quadratische gleichungen": {
"subsection": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"index": "1-3-3", "level": "subsection",
"refs": [[1, 3, 3, 0]],
}, },
"mengen": { "mengen": {
"subsection": "Mengen", "label": "Mengen",
"index": "1-1-1", "level": "section",
"refs": [[1, 1, 0, 0]],
}, },
}, },
), patch( ), patch(
"app.deterministic_services.socratic_oranisator.task_catalog.load_subsection_summaries", "app.deterministic_services.socratic_oranisator.task_catalog.load_topic_summaries",
return_value={ return_value={
"mengen": "Mengen summary text", "mengen": "Mengen summary text",
"quadratische gleichungen": "Quadratische summary text", "quadratische gleichungen": "Quadratische summary text",
...@@ -143,13 +145,15 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -143,13 +145,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
{ {
"subsection_key": "mengen", "subsection_key": "mengen",
"label": "Mengen", "label": "Mengen",
"refs": [[1, 1, 1]], "level": "section",
"refs": [[1, 1, 0, 0]],
"summary": "Mengen summary text", "summary": "Mengen summary text",
}, },
{ {
"subsection_key": "quadratische gleichungen", "subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]], "level": "subsection",
"refs": [[1, 3, 3, 0]],
"summary": "Quadratische summary text", "summary": "Quadratische summary text",
}, },
], ],
...@@ -177,13 +181,15 @@ class TaskCatalogSocraticTest(unittest.TestCase): ...@@ -177,13 +181,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
{ {
"subsection_key": "mengen", "subsection_key": "mengen",
"label": "Mengen", "label": "Mengen",
"refs": [[1, 1, 1]], "level": "section",
"refs": [[1, 1, 0, 0]],
"summary": "Mengen summary text", "summary": "Mengen summary text",
}, },
{ {
"subsection_key": "quadratische gleichungen", "subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]], "level": "subsection",
"refs": [[1, 3, 3, 0]],
"summary": "Quadratische summary text", "summary": "Quadratische summary text",
}, },
], ],
...@@ -289,8 +295,8 @@ Zweite Zeile. ...@@ -289,8 +295,8 @@ Zweite Zeile.
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
with patch( with patch(
"app.deterministic_services.task_catalog.load_subsection_map", "app.deterministic_services.task_catalog.load_topic_map",
return_value={"quadratische gleichungen": (1, 3, 3)}, return_value={"quadratische gleichungen": (1, 3, 3, 1)},
): ):
updated = task_catalog.select_subsection_by_ids( updated = task_catalog.select_subsection_by_ids(
sheet, sheet,
...@@ -301,20 +307,31 @@ Zweite Zeile. ...@@ -301,20 +307,31 @@ Zweite Zeile.
self.assertTrue(updated) self.assertTrue(updated)
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_refs"], [[1, 3, 3]]) self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["selected_subsection_refs"], [])
def test_select_subsection_by_key_sets_sheet_fields(self) -> None: def test_select_subsection_by_key_sets_sheet_fields(self) -> None:
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
with patch( with patch(
"app.deterministic_services.task_catalog.load_subsection_map", "app.deterministic_services.task_catalog.load_topic_map",
return_value={"quadratische gleichungen": (1, 3, 3)}, return_value={"quadratische gleichungen": (1, 3, 3, 1)},
): ):
updated = task_catalog.select_subsection_by_key(sheet, "quadratische_gleichungen") updated = task_catalog.select_subsection_by_key(sheet, "quadratische_gleichungen")
self.assertTrue(updated) self.assertTrue(updated)
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_refs"], [[1, 3, 3]]) self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["selected_subsection_refs"], [])
def test_get_selected_subsection_parent_refs_reads_normalized_refs(self) -> None:
sheet: dict[str, object] = {
"selected_subsection_parent_refs": [[1, 1, 0, 0], [1, 3, 2, 1], [1, 3, 2, 1]],
}
refs = task_catalog.get_selected_subsection_parent_refs(sheet)
self.assertEqual(refs, [(1, 1, 0, 0), (1, 3, 2, 1)])
def test_set_selected_task_stores_generic_parent_refs(self) -> None: def test_set_selected_task_stores_generic_parent_refs(self) -> None:
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
...@@ -428,7 +445,8 @@ class TaskApiSocraticTest(unittest.TestCase): ...@@ -428,7 +445,8 @@ class TaskApiSocraticTest(unittest.TestCase):
{ {
"subsection_key": "quadratische gleichungen", "subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen", "label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]], "level": "subsubsection",
"refs": [[1, 3, 3, 1]],
"summary": "Quadratische summary text", "summary": "Quadratische summary text",
} }
] ]
...@@ -498,10 +516,10 @@ class ChatBootstrapSocraticTest(unittest.TestCase): ...@@ -498,10 +516,10 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
), patch( ), patch(
"app.api.chat.context_store.set_initialized" "app.api.chat.context_store.set_initialized"
), patch( ), patch(
"app.api.chat.task_catalog.load_subsection_map", "app.api.chat.task_catalog.load_topic_map",
return_value={"quadratische gleichungen": (1, 3, 3)}, return_value={"quadratische gleichungen": (1, 3, 3, 1)},
), patch( ), patch(
"app.api.chat.retrieval_store.retrieve_for_subsections", "app.api.chat.retrieval_store.retrieve_for_parent_refs",
return_value=[], return_value=[],
) as retrieve_mock, patch( ) as retrieve_mock, patch(
"app.api.chat.socratic_oranisator.get_initial_message", "app.api.chat.socratic_oranisator.get_initial_message",
...@@ -517,8 +535,9 @@ class ChatBootstrapSocraticTest(unittest.TestCase): ...@@ -517,8 +535,9 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["reply"], "Was sind die Themen dieses Abschnitts?") self.assertEqual(response.json()["reply"], "Was sind die Themen dieses Abschnitts?")
retrieve_mock.assert_called_once_with(pg_url="postgresql://localhost/test", subsection_refs=[(1, 3, 3)]) retrieve_mock.assert_called_once_with(pg_url="postgresql://localhost/test", parent_refs=[(1, 3, 3, 1)])
self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen") self.assertEqual(sheet["selected_subsection_key"], "quadratische gleichungen")
self.assertEqual(sheet["selected_subsection_parent_refs"], [[1, 3, 3, 1]])
self.assertEqual(sheet["history"], [{"role": "assistant", "content": "Was sind die Themen dieses Abschnitts?"}]) self.assertEqual(sheet["history"], [{"role": "assistant", "content": "Was sind die Themen dieses Abschnitts?"}])
......
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