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,7 +5,11 @@ from typing import Iterable ...@@ -5,7 +5,11 @@ 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.
...@@ -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