From 8470e1e087a1fd98d3efc2891cfb4bd9d51841cf Mon Sep 17 00:00:00 2001 From: Klaude Date: Sun, 12 Jul 2026 18:42:54 +0300 Subject: [PATCH 1/8] CLAUDE.md: kirjaa MySQL:n migraatio geopalvelin1:lle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL ei ole enää paikallinen Docker-kontti vaan etäpalvelin geopalvelin1 (Tailscale, korkea latenssi); yhteysasetukset .env:stä. Muistutus kirjoittaa DB-koodi etälatenssia varten (pooli, COUNT/aggregaatit). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e64caee..8bc5f45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ Automaattinen pipeline suomalaisten yliopistojen opinto-oppaiden läpikäymiseen ## Tietovirrat - **Syöte:** opinto-oppaiden URL:t + suodatusasetukset (tiedekunta, taso, aihe) -- **Tallennus:** MySQL Docker-kontti (portti 21212) — kurssitiedot, LLM-vastaukset, mukaan/pois-päätökset +- **Tallennus:** MySQL (portti 21212; nyk. etäpalvelin geopalvelin1 / Tailscale — ks. Kehityskäytännöt) — kurssitiedot, LLM-vastaukset, mukaan/pois-päätökset - **LLM-kutsut:** seulontakysymyssarja (vaihe 2), arviointikysymyssarja (vaihe 3) - **Tuloste:** jäsennelty raportti + tilastot oppaiden laadusta @@ -60,7 +60,8 @@ WebUI:n esittely yleisölle (seminaari-lähiverkko ja etäkokous-Tailscale Funne ## Kehityskäytännöt - Python-projekti; pidä riippuvuudet minimissä ja kirjaa ne `requirements.txt`-tiedostoon -- MySQL Docker-kontissa (portti 21212) kaikelle pysyvyydelle; WebUI Docker-kontissa (portti 12121) +- MySQL kaikelle pysyvyydelle portissa 21212; WebUI Docker-kontissa (portti 12121) + - **Huom:** jaettu/tuotanto-MySQL on migroitu etäpalvelin **geopalvelin1**:lle (Tailscale-osoite `100.123.32.101:21212`, korkea/piikikäs latenssi) — **ei enää paikallinen Docker-kontti**. Yhteysasetukset `.env`:stä (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`); `docker-compose.yml`:n mysql-palvelu jää paikalliskehitykseen. Kirjoita tietokantakoodi etälatenssia varten: yhteyspooli (`tietokanta/yhteys.py`), `COUNT`/aggregaatit rivinouton sijaan, älä toista raskaita hakuja. - LLM-kutsut kulkevat yhden ohuen kääreen kautta (`llm/kutsu.py`), jotta malli/palveluntarjoaja voidaan vaihtaa — OpenAI-yhteensopiva rajapinta, konfiguraatio `.env`:ssä (`LLM_PROVIDER` = perus-URL, `LLM_API_KEY`, `LLM_MODEL`) - Promptit sijaitsevat omissa tiedostoissaan (ei koodin sisällä), jotta niitä voi iteroida koskematta logiikkaan - Hakurobottien täytyy olla kohteliaita: noudata `robots.txt`:ää, lisää viiveet, älä kuormita palvelimia From 5cd5b990e9090cc158aa6301f6b1a4cba09ae102 Mon Sep 17 00:00:00 2001 From: Klaude Date: Mon, 13 Jul 2026 06:47:47 +0300 Subject: [PATCH 2/8] =?UTF-8?q?arviointi:=20kirjaa=20ep=C3=A4onnistuneet?= =?UTF-8?q?=20er=C3=A4t=20loki.log-tiedostoon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiljainen `except Exception` nielaisi eräkohtaiset virheet, joten epäonnistuneiden arviointien syytä ei voinut diagnosoida. Kirjataan nyt aikaleima, poikkeustyyppi/-viesti ja erän kurssien KID:t loki.log:iin. Co-Authored-By: Claude Opus 4.8 --- arviointi/llmarviointi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index f24c66b..b5ff69b 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -1,5 +1,6 @@ """LLM-arviointi: lähettää mukaan otetut kurssit LLM:lle kysymysvastauksiin.""" import json +import time from tietokanta import mallit from llm import kutsu, tiiviste, kehoteet, kurssimuoto, asetukset @@ -235,8 +236,13 @@ def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, try: tulokset = _arvioi_erä(erä, arviointikehote, osa_kysymykset, jarjestelma) _tallenna_tulokset(tulokset, osa_kysymykset, malli, kys_tiiviste) - except Exception: + except Exception as e: # Viallinen erä ohitetaan; sen kurssit jäävät seuraavalle ajolle (idempotenssi). + kidt = [k.get("KID") for k in erä] + with open("loki.log", "a", encoding="utf-8") as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [arviointi] " + f"erä {erä_nro}/{len(erat)} epäonnistui ({type(e).__name__}: {e}); " + f"kurssit {kidt} jäävät seuraavalle ajolle\n") tulokset = [] for t in tulokset: arvioidut.add(t.get("id")) From 102ce9b30f4cb25d3171cb7b07db6ab49c9dbc0a Mon Sep 17 00:00:00 2001 From: Klaude Date: Mon, 13 Jul 2026 11:56:20 +0300 Subject: [PATCH 3/8] llm/arviointi: pakota JSON-tarjoaja ja salli ohjausmerkit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arviointierät kaatuivat massoittain JSON-virheisiin. Kaksi syytä korjattu: - kutsu.py: provider.require_parameters=true → OpenRouter reitittää vain tarjoajille jotka kunnioittavat response_formatia (poistaa "OpenInference"- tyyliset 502-roskavastaukset ja tyhjät vastaukset). - llmarviointi.py: json.loads(strict=False) → raa'at ohjausmerkit (\n, \t) perusteluissa eivät enää hylkää muuten kelvollista JSONia. Katkeamiset (Unterminated string) hoituvat pienemmällä ARVIOINTI_ERAKOKO:lla. Co-Authored-By: Claude Opus 4.8 --- arviointi/llmarviointi.py | 4 +++- llm/kutsu.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index b5ff69b..ecfa67a 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -23,7 +23,9 @@ def _erittele_json(teksti: str) -> list[dict]: if alku != -1: loppu = teksti.rfind("}") if loppu != -1: - data = json.loads(teksti[alku:loppu + 1]) + # strict=False sallii raa'at ohjausmerkit (\n, \t) merkkijonoissa — + # malli laittaa niitä perusteluihin, ei syytä hylätä muuten kelvollista JSONia. + data = json.loads(teksti[alku:loppu + 1], strict=False) for arvo in data.values(): if isinstance(arvo, list): return arvo diff --git a/llm/kutsu.py b/llm/kutsu.py index 835f29a..959d7d2 100644 --- a/llm/kutsu.py +++ b/llm/kutsu.py @@ -141,6 +141,10 @@ def kysy(viesti: str, jarjestelma: str = "", json_muoto: bool = False, } if json_muoto: runko["response_format"] = {"type": "json_object"} + # Reititä vain tarjoajille jotka oikeasti tukevat response_formatia — + # muuten OpenRouter voi valita tarjoajan joka ohittaa sen ja palauttaa + # roskaa/tyhjää (ks. lokin "OpenInference"-502:t). OpenRouter-kohtainen. + runko["provider"] = {"require_parameters": True} vastaus = requests.post( f"{perus_url.rstrip('/')}/chat/completions", headers={"Authorization": f"Bearer {api_avain}"}, From 12d07c9c6c341a544241e8ce6d95ee5d52b9d190 Mon Sep 17 00:00:00 2001 From: Klaude Date: Mon, 13 Jul 2026 12:15:51 +0300 Subject: [PATCH 4/8] =?UTF-8?q?arviointi:=20torju=20v=C3=A4=C3=A4r=C3=A4t?= =?UTF-8?q?=20id:t=20ja=20kirjaa=20puuttuvat=20tulokset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structured-output-oppaan kohta 6: aseta_vastaus on suora INSERT (KysID, KID), joten mallin hallusinoima id kirjoittaisi orpo-/väärän rivin. _siivoa_tulokset suodattaa tulokset vain erän kursseihin ja normalisoi id:t kokonaisluvuiksi. Lisäksi loki.log:iin kirjataan kun malli palauttaa vähemmän kursseja kuin pyydettiin (hiljainen osittaisvaje näkyviin). Loki-kirjoitus DRY:ttiin _kirjaa-apufunktioon. Co-Authored-By: Claude Opus 4.8 --- arviointi/llmarviointi.py | 38 ++++++++++++++++++++++++++++++++------ testit/test_arviointi.py | 16 ++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index ecfa67a..0b2bf36 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -12,6 +12,29 @@ def erakoko() -> int: return asetukset.lue_int("ARVIOINTI_ERAKOKO", _OLETUS_ERAKOKO) +def _kirjaa(viesti: str) -> None: + """Lisää aikaleimattu rivi loki.log:iin (eräkohtaiset virheet/varoitukset).""" + with open("loki.log", "a", encoding="utf-8") as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [arviointi] {viesti}\n") + + +def _siivoa_tulokset(raaka: list[dict], odotetut: set) -> list[dict]: + """Suodattaa mallin tulokset vain erän kursseihin ja normalisoi id:t kokonaisluvuiksi. + + Torjuu hallusinoidut / väärät id:t ennen tallennusta (opas: 'IDs must exist') — + muuten aseta_vastaus INSERTtäisi rivin kurssille jota erässä ei ollut. + """ + siivotut = [] + for t in raaka: + try: + kid = int(t.get("id")) + except (TypeError, ValueError): + continue + if kid in odotetut: + siivotut.append({**t, "id": kid}) + return siivotut + + def _lue_jarjestelma_kehote() -> str: return kehoteet.lue("arviointi_jarjestelma.txt") @@ -235,16 +258,19 @@ def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, for erä_nro, (osa_kysymykset, erä) in enumerate(erat, 1): if edistyminen_cb: edistyminen_cb(käsitelty, yhteensa, erä_nro, len(erat)) + odotetut = {k["KID"] for k in erä} try: - tulokset = _arvioi_erä(erä, arviointikehote, osa_kysymykset, jarjestelma) + raaka = _arvioi_erä(erä, arviointikehote, osa_kysymykset, jarjestelma) + tulokset = _siivoa_tulokset(raaka, odotetut) _tallenna_tulokset(tulokset, osa_kysymykset, malli, kys_tiiviste) + puuttuvat = odotetut - {t["id"] for t in tulokset} + if puuttuvat: + _kirjaa(f"erä {erä_nro}/{len(erat)}: malli palautti {len(tulokset)}/{len(odotetut)} " + f"kurssia; puuttuvat {sorted(puuttuvat)} jäävät seuraavalle ajolle") except Exception as e: # Viallinen erä ohitetaan; sen kurssit jäävät seuraavalle ajolle (idempotenssi). - kidt = [k.get("KID") for k in erä] - with open("loki.log", "a", encoding="utf-8") as f: - f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [arviointi] " - f"erä {erä_nro}/{len(erat)} epäonnistui ({type(e).__name__}: {e}); " - f"kurssit {kidt} jäävät seuraavalle ajolle\n") + _kirjaa(f"erä {erä_nro}/{len(erat)} epäonnistui ({type(e).__name__}: {e}); " + f"kurssit {sorted(odotetut)} jäävät seuraavalle ajolle") tulokset = [] for t in tulokset: arvioidut.add(t.get("id")) diff --git a/testit/test_arviointi.py b/testit/test_arviointi.py index 65133ef..f5d3bfb 100644 --- a/testit/test_arviointi.py +++ b/testit/test_arviointi.py @@ -369,3 +369,19 @@ def test_useita_kursseja_ja_kysymyksia(self): ] llmarviointi._tallenna_tulokset(tulokset, ks, "m") assert mock_aseta.call_count == 4 + + +class TestSiivoaTulokset: + """Kohta 6: torju hallusinoidut/väärät id:t, säilytä vain erän kurssit.""" + + def test_suodattaa_vieraan_idn(self): + raaka = [{"id": 1, "vastaukset": []}, {"id": 999, "vastaukset": []}] + assert llmarviointi._siivoa_tulokset(raaka, {1, 2}) == [{"id": 1, "vastaukset": []}] + + def test_normalisoi_merkkijono_idn(self): + tulos = llmarviointi._siivoa_tulokset([{"id": "2", "vastaukset": []}], {1, 2}) + assert tulos == [{"id": 2, "vastaukset": []}] + + def test_ohittaa_puuttuvan_tai_kelvottoman_idn(self): + raaka = [{"vastaukset": []}, {"id": None, "vastaukset": []}, {"id": "abc"}] + assert llmarviointi._siivoa_tulokset(raaka, {1, 2}) == [] From fd23d6625139d330aa984cbfad484a448b81b90b Mon Sep 17 00:00:00 2001 From: Klaude Date: Mon, 13 Jul 2026 12:33:09 +0300 Subject: [PATCH 5/8] =?UTF-8?q?LLM-asetukset:=20n=C3=A4yt=C3=A4=20ja=20var?= =?UTF-8?q?oita=20mallin=20muototuesta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saatavuustarkistus varmisti vain että mallin id löytyy /models-listalta, ei sitä tukeeko malli response_formatia. OpenRouterin /models-vastauksessa on jo per-malli supported_parameters — luetaan se ilman lisäpyyntöä: - muoto_kentta() + uusi "muoto?"-sarake mallitaulukkoon (skeema/json/tools/—/?) - muototuki_varoitus() + "Tarkista saatavuus" varoittaa jos nykyinen malli ei ilmoita response_format-tukea (silloin require_parameters ei voi auttaa) Ei suodata malleja pois — vain näyttää tuen, jotta mallinvalinta on itsedokumentoiva sen sijaan että vika näkyy vasta loki.log:ssa. Co-Authored-By: Claude Opus 4.8 --- cliui/asetuksetnaytto.py | 9 ++++++- llm/mallitiedot.py | 51 ++++++++++++++++++++++++++++++++------ testit/test_mallitiedot.py | 31 ++++++++++++++++++++++- 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/cliui/asetuksetnaytto.py b/cliui/asetuksetnaytto.py index 56464b5..5936d70 100644 --- a/cliui/asetuksetnaytto.py +++ b/cliui/asetuksetnaytto.py @@ -34,9 +34,16 @@ def _tarkista(stdscr) -> None: stdscr.refresh() try: mallitiedot.tarkista_saatavuus() - nayta_viesti(stdscr, f"Malli '{kutsu.hae_malli()}' on saatavilla.", 4) except Exception as e: nayta_viesti(stdscr, f"Ei saatavilla: {e}", 4) + return + rivit = [f"Malli '{kutsu.hae_malli()}' on saatavilla."] + varoitus = mallitiedot.muototuki_varoitus() + if varoitus: + rivit += ["", f"VAROITUS: {varoitus}"] + for i, teksti in enumerate(rivit): + stdscr.addstr(4 + i, 0, teksti) + nayta_viesti(stdscr, "", 4 + len(rivit) + 1) def _paivita_lista(stdscr) -> None: diff --git a/llm/mallitiedot.py b/llm/mallitiedot.py index 2448eb0..bea3496 100644 --- a/llm/mallitiedot.py +++ b/llm/mallitiedot.py @@ -67,6 +67,39 @@ def tukee_valimuistia(malli_tiedot: dict) -> bool: return "input_cache_read" in (malli_tiedot.get("pricing") or {}) +def muoto_kentta(malli_tiedot: dict) -> str: + """Lyhyt merkintä mallin rakenteisen ulostulon tuesta (OpenRouterin + supported_parameters) — vahvin ensin. Tyhjä kenttä → '?' (ei tietoa).""" + tuetut = malli_tiedot.get("supported_parameters") + if not tuetut: + return "?" + tuetut = set(tuetut) + if "structured_outputs" in tuetut: + return "skeema" + if "response_format" in tuetut: + return "json" + if "tools" in tuetut: + return "tools" + return "—" + + +def muototuki_varoitus(malli: str | None = None) -> str | None: + """Varoitusteksti jos malli EI ilmoita tukevansa response_formatia, muuten None. + + Perustuu jo haetun /models-listan supported_parameters-kenttään — ei uutta + pyyntöä. Palauttaa None jos mallia ei löydy tai kenttää ei ole (ei voida + päätellä; saatavuus tarkistetaan erikseen).""" + malli = malli or os.environ.get("LLM_MODEL", "") + tiedot = hae_malli_tiedot(malli) + tuetut = tiedot.get("supported_parameters") if tiedot else None + if not tuetut: + return None + if "response_format" not in set(tuetut): + return ("Malli ei ilmoita tukevansa response_formatia — JSON-tila ei " + "välttämättä toimi. Valitse malli jonka 'muoto?' on json tai skeema.") + return None + + def _hinta_per_milj(arvo: str | None) -> float: """OpenRouterin per-token-hinta ($/token) → $/miljoona tokenia.""" try: @@ -75,20 +108,21 @@ def _hinta_per_milj(arvo: str | None) -> float: return 0.0 -_SARAKEOTSIKOT = ("Mallin nimi", "Hinta", "konteksti", "välimuisti?") +_SARAKEOTSIKOT = ("Mallin nimi", "Hinta", "konteksti", "muoto?", "välimuisti?") _EROTIN = " | " -def _mallin_kentat(malli_tiedot: dict) -> tuple[str, str, str, str]: - """Yhden mallin sarakearvot: (id, hinta, konteksti, välimuisti).""" +def _mallin_kentat(malli_tiedot: dict) -> tuple[str, str, str, str, str]: + """Yhden mallin sarakearvot: (id, hinta, konteksti, muoto, välimuisti).""" pricing = malli_tiedot.get("pricing") or {} sisaan = _hinta_per_milj(pricing.get("prompt")) ulos = _hinta_per_milj(pricing.get("completion")) hinta = "ilmainen" if sisaan == 0 and ulos == 0 else f"${sisaan:.2f}/${ulos:.2f} per Mtok" ctx = malli_tiedot.get("context_length") or 0 konteksti = f"{ctx // 1000}k" if ctx else "?" + muoto = muoto_kentta(malli_tiedot) valimuisti = "kyllä" if tukee_valimuistia(malli_tiedot) else "—" - return malli_tiedot.get("id", "?"), hinta, konteksti, valimuisti + return malli_tiedot.get("id", "?"), hinta, konteksti, muoto, valimuisti def muotoile_taulukko(mallit: list[dict]) -> tuple[list[str], list[str]]: @@ -99,14 +133,15 @@ def muotoile_taulukko(mallit: list[dict]) -> tuple[list[str], list[str]]: keskelle, välimuisti vasemmalle (viimeinen sarake). Kaikki rivit ovat samanlevyisiä. """ kentat = [_mallin_kentat(m) for m in mallit] - lev = [len(_SARAKEOTSIKOT[s]) for s in range(4)] + n = len(_SARAKEOTSIKOT) + lev = [len(_SARAKEOTSIKOT[s]) for s in range(n)] for rivi in kentat: - for s in range(4): + for s in range(n): lev[s] = max(lev[s], len(rivi[s])) - def koosta(nimi, hinta, ktx, valimuisti): + def koosta(nimi, hinta, ktx, muoto, valimuisti): return _EROTIN.join([nimi.ljust(lev[0]), hinta.ljust(lev[1]), - ktx.center(lev[2]), valimuisti.ljust(lev[3])]) + ktx.center(lev[2]), muoto.ljust(lev[3]), valimuisti.ljust(lev[4])]) otsikkorivi = koosta(*_SARAKEOTSIKOT) viiva = "-" * len(otsikkorivi) diff --git a/testit/test_mallitiedot.py b/testit/test_mallitiedot.py index 0a15b56..87c15ad 100644 --- a/testit/test_mallitiedot.py +++ b/testit/test_mallitiedot.py @@ -102,12 +102,41 @@ def test_tunnistaa_cache_kentan(self): assert mallitiedot.tukee_valimuistia(_DATA[0]) is False +class TestMuoto: + def test_muoto_kentta_vahvin_ensin(self): + assert mallitiedot.muoto_kentta({"supported_parameters": ["response_format", "structured_outputs"]}) == "skeema" + assert mallitiedot.muoto_kentta({"supported_parameters": ["response_format"]}) == "json" + assert mallitiedot.muoto_kentta({"supported_parameters": ["tools", "tool_choice"]}) == "tools" + assert mallitiedot.muoto_kentta({"supported_parameters": ["temperature"]}) == "—" + + def test_muoto_kentta_tuntematon_kun_kentta_puuttuu(self): + assert mallitiedot.muoto_kentta({}) == "?" + + def test_varoitus_kun_ei_response_formatia(self): + data = [{"id": "m/x", "supported_parameters": ["tools"]}] + with patch.dict(os.environ, {**_ENV, "LLM_MODEL": "m/x"}), \ + patch("llm.mallitiedot.requests.get", return_value=_mock_get(data)): + assert mallitiedot.muototuki_varoitus() is not None + + def test_ei_varoitusta_kun_response_format_tuettu(self): + data = [{"id": "m/x", "supported_parameters": ["response_format"]}] + with patch.dict(os.environ, {**_ENV, "LLM_MODEL": "m/x"}), \ + patch("llm.mallitiedot.requests.get", return_value=_mock_get(data)): + assert mallitiedot.muototuki_varoitus() is None + + def test_ei_varoitusta_kun_tietoa_ei_ole(self): + data = [{"id": "m/x"}] # ei supported_parameters -kenttää + with patch.dict(os.environ, {**_ENV, "LLM_MODEL": "m/x"}), \ + patch("llm.mallitiedot.requests.get", return_value=_mock_get(data)): + assert mallitiedot.muototuki_varoitus() is None + + class TestTaulukko: def test_otsikkorivit_sisaltavat_sarakeotsikot_ja_viivan(self): otsikkorivit, _ = mallitiedot.muotoile_taulukko(_DATA) assert len(otsikkorivit) == 2 otsikko, viiva = otsikkorivit - for sarake in ("Mallin nimi", "Hinta", "konteksti", "välimuisti?"): + for sarake in ("Mallin nimi", "Hinta", "konteksti", "muoto?", "välimuisti?"): assert sarake in otsikko assert set(viiva) == {"-"} # erotinviiva pelkkiä viivoja assert len(viiva) == len(otsikko) # yhtä leveä kuin otsikkorivi From efd2f38e6572f4af611417c1d279f4ea47c11397 Mon Sep 17 00:00:00 2001 From: Klaude Date: Mon, 13 Jul 2026 16:41:10 +0300 Subject: [PATCH 6/8] =?UTF-8?q?Arviointin=C3=A4ytt=C3=B6:=20erottele=20t?= =?UTF-8?q?=C3=A4ydennys/muutos,=20n=C3=A4yt=C3=A4=20tilastot,=20keskeytys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neljä parannusta arvioinnin ajonäkymään: 1. laske_tyomaara palauttaa (uudet, täydennettävät, muuttuneet) — pelkkä puuttuva vastaus ei enää valehtele "kehote muuttunut" -varoituksena; varoitetaan vain kun tiiviste oikeasti eroaa. 2. Näyttö tyhjennetään joka ruudulla (stdscr.erase) — ei vanhaa valikkotekstiä roikkumaan "Yhdistetään LLM:ään..." perään. 3. aja() luokittelee epäonnistuneet erät (max_tokens/502/tyhjä/muoto) ja pitää elävää tilastoa; näyttö listaa onnistuneet + epäonnistuneet syittäin. 4. keskeyta_cb + stdscr.nodelay: 'q' keskeyttää ajon erien välissä ilman Ctrl-C:tä. Co-Authored-By: Claude Opus 4.8 --- arviointi/llmarviointi.py | 67 +++++++++++++++++++++------ cliui/arviointinaytto.py | 97 ++++++++++++++++++++++++++++----------- testit/test_arviointi.py | 76 ++++++++++++++++++++++++++---- 3 files changed, 191 insertions(+), 49 deletions(-) diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index 0b2bf36..c4f1b59 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -35,6 +35,22 @@ def _siivoa_tulokset(raaka: list[dict], odotetut: set) -> list[dict]: return siivotut +def _luokittele_virhe(e: Exception) -> str: + """Eräkohtainen virhe → tilastoluokka (max_tokens / virhe_502 / tyhja / muoto). + + max_tokens tunnistetaan viimeisimmän kutsun finish_reasonista ('length') — + vastaus tuli, mutta katkesi token-kattoon → JSON jää vajaaksi. Tarkistetaan + vasta muut luokat suljettua, jolloin finish_reason on tuoreesta pyynnöstä.""" + teksti = str(e) + if "tyhjän vastauksen" in teksti or "tyhjän choices" in teksti: + return "tyhja" + if "502" in teksti or isinstance(e, RuntimeError): + return "virhe_502" + if kutsu.hae_viimeisin_kaytto().get("finish_reason") == "length": + return "max_tokens" + return "muoto" + + def _lue_jarjestelma_kehote() -> str: return kehoteet.lue("arviointi_jarjestelma.txt") @@ -185,25 +201,36 @@ def _kysymystiivisteet(tutkimus: dict) -> dict: tutkimus["Arviointikehote"], _lue_jarjestelma_kehote(), kysymykset) -def laske_tyomaara(tutkimus: dict, tieto: dict | None = None) -> tuple[int, int]: - """(uudet, vanhentuneet) — montako mukana-kurssia arvioidaan: täysin uudet - (ei aiempia vastauksia) vs. osin vanhentuneet (kehote/kysymys muuttunut). +def laske_tyomaara(tutkimus: dict, tieto: dict | None = None) -> tuple[int, int, int]: + """(uudet, täydennettävät, muuttuneet) — montako mukana-kurssia arvioidaan: + - uudet: ei aiempia vastauksia lainkaan + - täydennettävät: osa vastauksista on, puuttuvat kysymykset vain täytetään + (kehote/kysymys EI muuttunut) — esim. malli palautti erässä vajaat tulokset + - muuttuneet: jokin jo vastattu kysymys on vanhentunut (kehote/kysymys muuttui) tieto: esilaskettu _selvita_tyo-tulos — annettuna vältetään kurssien ja vastaustiivisteiden uudelleenhaku (merkitsevää etäpalvelimella).""" if tieto is None: tieto = _selvita_tyo(tutkimus) - uudet = vanhentuneet = 0 - for kid in tieto["tyo"]: + olemassa = tieto["olemassa"] + kys_tiiviste = tieto["kys_tiiviste"] + uudet = taydennettavat = muuttuneet = 0 + for kid, tarvittavat in tieto["tyo"].items(): oli_vastauksia = any( - (s := tieto["olemassa"].get((kid, k["KysID"]))) and s["vastattu"] + (s := olemassa.get((kid, k["KysID"]))) and s["vastattu"] for k in tieto["kysymykset"] ) - if oli_vastauksia: - vanhentuneet += 1 - else: + if not oli_vastauksia: uudet += 1 - return uudet, vanhentuneet + elif any( + (s := olemassa.get((kid, k["KysID"]))) and s["vastattu"] + and s["tiiviste"] != kys_tiiviste[k["KysID"]] + for k in tarvittavat + ): + muuttuneet += 1 + else: + taydennettavat += 1 + return uudet, taydennettavat, muuttuneet def rakenna_erat(tieto: dict, koko: int) -> list[tuple[list[dict], list[dict]]]: @@ -226,7 +253,7 @@ def rakenna_erat(tieto: dict, koko: int) -> list[tuple[list[dict], list[dict]]]: def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, - tieto: dict | None = None) -> int: + tieto: dict | None = None, keskeyta_cb=None) -> int: """Arvioi mukaan otetut kurssit LLM:llä. Palauttaa arvioitujen kurssien määrän. Idempotentti ja kehotetietoinen: kysyy LLM:ltä vain ne (kurssi, kysymys) -parit, @@ -235,6 +262,10 @@ def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, max_erat: jos annettu, ajetaan korkeintaan tämän verran eräpyyntöjä (esim. 1 = yksi LLM-pyyntö, kätevä testaukseen). Loput jäävät seuraavalle ajolle. tieto: esilaskettu _selvita_tyo-tulos — annettuna vältetään uudelleenhaku. + keskeyta_cb: jos annettu ja palauttaa True erien välissä, ajo lopetetaan + siististi (loput jäävät seuraavalle ajolle). + edistyminen_cb(käsitelty, yhteensa, erä_nro, erien_maara, tilasto): tilasto on + elävä dict {onnistuneet, max_tokens, virhe_502, tyhja, muoto}. """ if tieto is None: tieto = _selvita_tyo(tutkimus) @@ -254,10 +285,13 @@ def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, yhteensa = sum(len(erä) for _, erä in erat) arvioidut: set = set() käsitelty = 0 + tilasto = {"onnistuneet": 0, "max_tokens": 0, "virhe_502": 0, "tyhja": 0, "muoto": 0} for erä_nro, (osa_kysymykset, erä) in enumerate(erat, 1): + if keskeyta_cb and keskeyta_cb(): + break if edistyminen_cb: - edistyminen_cb(käsitelty, yhteensa, erä_nro, len(erat)) + edistyminen_cb(käsitelty, yhteensa, erä_nro, len(erat), tilasto) odotetut = {k["KID"] for k in erä} try: raaka = _arvioi_erä(erä, arviointikehote, osa_kysymykset, jarjestelma) @@ -269,13 +303,16 @@ def aja(tutkimus: dict, edistyminen_cb=None, max_erat: int | None = None, f"kurssia; puuttuvat {sorted(puuttuvat)} jäävät seuraavalle ajolle") except Exception as e: # Viallinen erä ohitetaan; sen kurssit jäävät seuraavalle ajolle (idempotenssi). + kategoria = _luokittele_virhe(e) + tilasto[kategoria] += len(erä) _kirjaa(f"erä {erä_nro}/{len(erat)} epäonnistui ({type(e).__name__}: {e}); " - f"kurssit {sorted(odotetut)} jäävät seuraavalle ajolle") + f"kurssit {sorted(odotetut)} jäävät seuraavalle ajolle [{kategoria}]") tulokset = [] for t in tulokset: - arvioidut.add(t.get("id")) + arvioidut.add(t["id"]) + tilasto["onnistuneet"] = len(arvioidut) käsitelty += len(erä) if edistyminen_cb: - edistyminen_cb(käsitelty, yhteensa, erä_nro, len(erat)) + edistyminen_cb(käsitelty, yhteensa, erä_nro, len(erat), tilasto) return len(arvioidut) diff --git a/cliui/arviointinaytto.py b/cliui/arviointinaytto.py index 812d851..7c76eb2 100644 --- a/cliui/arviointinaytto.py +++ b/cliui/arviointinaytto.py @@ -2,6 +2,45 @@ from tietokanta import mallit from cliui.apurit import piirra_otsikko, nayta_viesti, valitse_listasta, lue_teksti +# Tilaston rivit: (avain aja()n tilastodictissä, näyttöteksti). Sama järjestys +# elävässä edistymisnäytössä ja loppuyhteenvedossa. +_TILASTORIVIT = [ + ("onnistuneet", "Onnistuneita arviointeja"), + ("max_tokens", "Epäonnistuneita arviointeja (max tokens)"), + ("virhe_502", "Epäonnistuneita arviointeja (502 virhe)"), + ("tyhja", "Epäonnistuneita arviointeja (tyhjä vastaus)"), + ("muoto", "Epäonnistuneita arviointeja (vastaus ei oikean muotoinen)"), +] +_TYHJA_TILASTO = {avain: 0 for avain, _ in _TILASTORIVIT} + + +def _piirra_tilasto(stdscr, tilasto: dict, alkurivi: int) -> None: + for i, (avain, nimi) in enumerate(_TILASTORIVIT): + stdscr.addstr(alkurivi + i, 0, f" * {tilasto.get(avain, 0):>3} {nimi}") + + +def _piirra_edistyminen(stdscr, otsikko: str, tila_rivi: str, tilasto: dict) -> None: + """Tyhjentää ja piirtää koko edistymisnäytön (ei jää roikkumaan vanhaa tekstiä).""" + stdscr.erase() + piirra_otsikko(stdscr, otsikko) + stdscr.addstr(3, 0, "Yhdistetään LLM:ään...") + stdscr.addstr(4, 0, f" {tila_rivi}") + _piirra_tilasto(stdscr, tilasto, 6) + stdscr.addstr(6 + len(_TILASTORIVIT) + 1, 0, "paina q keskeyttääksesi") + stdscr.refresh() + + +def _nayta_yhteenveto(stdscr, otsikko: str, keskeytetty: bool, tilasto: dict, yhteensa: int) -> None: + stdscr.erase() + piirra_otsikko(stdscr, f"{otsikko} — {'keskeytetty' if keskeytetty else 'valmis'}") + _piirra_tilasto(stdscr, tilasto, 3) + rivi = 3 + len(_TILASTORIVIT) + 1 + kesken = yhteensa - tilasto.get("onnistuneet", 0) + if kesken > 0: + stdscr.addstr(rivi, 0, f"Jäljellä vielä {kesken} kurssia (aja uudelleen jatkaaksesi).") + rivi += 1 + nayta_viesti(stdscr, "", rivi + 1) + def nayta(stdscr) -> None: tutkimukset = mallit.hae_tutkimukset() @@ -70,19 +109,20 @@ def _aja_llm(stdscr, tutkimus: dict, vain_yksi_era: bool = False) -> None: # työmäärälaskennalle ja ajolle — muuten _selvita_tyo (kaikki kurssit + # vastaustiivisteet) ajettaisiin 2–3 kertaa peräkkäin etäpalvelinta vasten. tieto = llmarviointi._selvita_tyo(tutkimus) - uudet, vanhentuneet = llmarviointi.laske_tyomaara(tutkimus, tieto=tieto) - if uudet + vanhentuneet == 0: + uudet, taydennettavat, muuttuneet = llmarviointi.laske_tyomaara(tutkimus, tieto=tieto) + yhteensa = uudet + taydennettavat + muuttuneet + if yhteensa == 0: nayta_viesti(stdscr, "Kaikki mukana olevat kurssit on jo arvioitu nykyisellä kehotteella ja kysymyksillä.") return - # Yhden erän testiajo: ohitetaan kustannusvaroitus (yksi pyyntö on tarkoituksellisen pieni). - # Täysajossa varoitetaan, jos kehote/kysymys on muuttunut (uudelleenajo maksaa). - if not vain_yksi_era and vanhentuneet > 0: + # Varoitetaan vain jos kehote/kysymys OIKEASTI muuttui (uudelleenajo maksaa). + # Pelkkä täydennys (puuttuvia vastauksia) ei ole muutos → ei varoitusta. + if not vain_yksi_era and muuttuneet > 0: valinta = valitse_listasta( stdscr, "LLM-arviointi — kehote tai kysymys on muuttunut", [ - f"Aja LLM: {uudet} uutta + {vanhentuneet} uudelleen (muutos) — vain muuttuneet kysymykset", + f"Aja LLM: {uudet} uutta + {taydennettavat} täydennys + {muuttuneet} uudelleen (muutos)", "Peruuta", ], ) @@ -96,32 +136,37 @@ def _aja_llm(stdscr, tutkimus: dict, vain_yksi_era: bool = False) -> None: nayta_viesti(stdscr, f"Mallia ei voi käyttää: {e}") return - stdscr.addstr(3, 0, "Yhdistetään LLM:ään...") - stdscr.refresh() + otsikko_ajo = f"{otsikko} — {tutkimus['LuokittelunNimi']}" + saatu = [dict(_TYHJA_TILASTO)] # viimeisin tilasto talteen loppunäyttöä varten + keskeytetty = [False] - _odottaa = [True] + def edistyminen(n, yht, erä, erat, tilasto): + saatu[0] = tilasto + _piirra_edistyminen(stdscr, otsikko_ajo, f"Erä {erä}/{erat} — lähettää... ({n}/{yht} käsitelty)", tilasto) - def edistyminen(n, yht, erä, erat): - if _odottaa[0]: - stdscr.addstr(4, 0, f" Erä {erä}/{erat} — lähettää... ({n}/{yht} käsitelty) ") - else: - stdscr.addstr(4, 0, f" Erä {erä}/{erat} — {n}/{yht} käsitelty ") - _odottaa[0] = not _odottaa[0] - stdscr.refresh() + def keskeyta(): + if stdscr.getch() in (ord("q"), ord("Q")): + keskeytetty[0] = True + return True + return False + # nodelay(True): getch ei blokkaa → 'q' luetaan erien välissä keskeyttämättä ajoa. + stdscr.nodelay(True) + virhe = None try: - arvioitu = llmarviointi.aja(tutkimus, edistyminen, - max_erat=1 if vain_yksi_era else None, tieto=tieto) - piirra_otsikko(stdscr, "LLM-arviointi — valmis") - stdscr.addstr(3, 0, f"Arvioitu: {arvioitu}") - if vain_yksi_era and arvioitu < uudet + vanhentuneet: - jaljella = uudet + vanhentuneet - arvioitu - stdscr.addstr(4, 0, f"Jäljellä vielä {jaljella} kurssia (aja uudelleen jatkaaksesi).") - nayta_viesti(stdscr, "", 6) + llmarviointi.aja(tutkimus, edistyminen, max_erat=1 if vain_yksi_era else None, + tieto=tieto, keskeyta_cb=keskeyta) except EnvironmentError as e: - nayta_viesti(stdscr, f"Virhe: {e}") + virhe = f"Virhe: {e}" except Exception as e: - nayta_viesti(stdscr, f"LLM-virhe: {e}") + virhe = f"LLM-virhe: {e}" + finally: + stdscr.nodelay(False) + + if virhe: + nayta_viesti(stdscr, virhe) + return + _nayta_yhteenveto(stdscr, otsikko, keskeytetty[0], saatu[0], yhteensa) def _aja_testiera(stdscr, tutkimus: dict) -> None: diff --git a/testit/test_arviointi.py b/testit/test_arviointi.py index f5d3bfb..a762487 100644 --- a/testit/test_arviointi.py +++ b/testit/test_arviointi.py @@ -142,7 +142,7 @@ def test_aja_kutsuu_kysy_json_muodolla(self): def test_aja_kutsuu_edistyminen_cb(self): tapahtumat = [] - def edistyminen(n, yht, erä, erat): + def edistyminen(n, yht, erä, erat, tilasto): tapahtumat.append((n, yht, erä, erat)) with patch("arviointi.llmarviointi.mallit.hae_kysymykset", return_value=KYSYMYKSET), \ @@ -173,7 +173,7 @@ def test_aja_max_erat_rajaa_yhteen_pyyntoon(self): patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): - llmarviointi.aja(TUTKIMUS, lambda n, y, e, et: tapahtumat.append(et), max_erat=1) + llmarviointi.aja(TUTKIMUS, lambda n, y, e, et, ti: tapahtumat.append(et), max_erat=1) assert mock_kysy.call_count == 1 assert all(et == 1 for et in tapahtumat) # eräkokonaismäärä näkyy rajattuna @@ -204,9 +204,10 @@ def test_laske_tyomaara_erottaa_uudet_ja_vanhentuneet(self): patch("arviointi.llmarviointi.mallit.hae_valitut_kurssit", return_value=KURSSIT), \ patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value=olemassa), \ patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value=jarj): - uudet, vanhentuneet = llmarviointi.laske_tyomaara(TUTKIMUS) - assert uudet == 1 - assert vanhentuneet == 1 + uudet, taydennettavat, muuttuneet = llmarviointi.laske_tyomaara(TUTKIMUS) + assert uudet == 1 # kurssi 2: ei vastauksia + assert taydennettavat == 0 + assert muuttuneet == 1 # kurssi 1: kysymys 11 vanhentunut (tiiviste eroaa) def test_kysymystiivisteet_ei_hae_kursseja_eika_vastauksia(self): """Kevyt tiivistehaku käyttää vain hae_kysymykset — ei kurssien/vastausten @@ -224,12 +225,14 @@ def test_kysymystiivisteet_ei_hae_kursseja_eika_vastauksia(self): def test_laske_tyomaara_esilasketulla_tiedolla_ei_hae_uudelleen(self): """Annettu tieto → ei uutta _selvita_tyo-hakua (ei kurssien/vastausten latausta).""" tieto = {"kysymykset": KYSYMYKSET, - "olemassa": {(1, 10): {"tiiviste": "x", "vastattu": True}}, + "kys_tiiviste": {10: "x10", 11: "x11"}, + "olemassa": {(1, 10): {"tiiviste": "x10", "vastattu": True}}, "tyo": {1: [KYSYMYKSET[1]], 2: list(KYSYMYKSET)}} with patch("arviointi.llmarviointi._selvita_tyo") as mock_selvita: - uudet, vanhentuneet = llmarviointi.laske_tyomaara(TUTKIMUS, tieto=tieto) + uudet, taydennettavat, muuttuneet = llmarviointi.laske_tyomaara(TUTKIMUS, tieto=tieto) mock_selvita.assert_not_called() - assert (uudet, vanhentuneet) == (1, 1) # kurssi 1: oli vastauksia; kurssi 2: uusi + # kurssi 1: kysymys 11 vain puuttuu (10 ajan tasalla) → täydennys; kurssi 2: uusi + assert (uudet, taydennettavat, muuttuneet) == (1, 1, 0) def test_aja_esilasketulla_tiedolla_ei_hae_kursseja(self): """aja(tieto=...) ei kutsu _selvita_tyo:tä → ei hae_valitut_kurssit-latausta.""" @@ -253,6 +256,63 @@ def test_aja_esilasketulla_tiedolla_ei_hae_kursseja(self): mk.assert_not_called(); mvk.assert_not_called(); mvt.assert_not_called() +class TestLuokitteleVirhe: + def test_tyhja_vastaus(self): + e = ValueError("LLM palautti tyhjän vastauksen (finish_reason: stop)") + assert llmarviointi._luokittele_virhe(e) == "tyhja" + + def test_502(self): + e = RuntimeError("OpenRouter-virhe (502): Upstream error") + assert llmarviointi._luokittele_virhe(e) == "virhe_502" + + def test_max_tokens_finish_reasonista(self): + import json as _json + e = _json.JSONDecodeError("Unterminated string", "doc", 5) + with patch("arviointi.llmarviointi.kutsu.hae_viimeisin_kaytto", + return_value={"finish_reason": "length"}): + assert llmarviointi._luokittele_virhe(e) == "max_tokens" + + def test_muoto_kun_ei_length(self): + e = ValueError("Ei JSON-objektia vastauksessa: xyz") + with patch("arviointi.llmarviointi.kutsu.hae_viimeisin_kaytto", + return_value={"finish_reason": "stop"}): + assert llmarviointi._luokittele_virhe(e) == "muoto" + + +@pytest.fixture +def aja_ymparisto(): + """Mockaa aja():n DB- ja kehote-riippuvuudet; testi asettaa vain kutsu.kysy:n.""" + with patch("arviointi.llmarviointi.mallit.hae_kysymykset", return_value=KYSYMYKSET), \ + patch("arviointi.llmarviointi.mallit.hae_valitut_kurssit", return_value=KURSSIT), \ + patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value={}), \ + patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ + patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ + patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + yield + + +class TestAjaTilastoJaKeskeytys: + def test_tilasto_kirjaa_onnistuneet(self, aja_ymparisto): + tilastot = [] + with patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS): + llmarviointi.aja(TUTKIMUS, lambda n, y, e, et, ti: tilastot.append(dict(ti))) + assert tilastot[-1]["onnistuneet"] == 2 + + def test_tilasto_luokittelee_epaonnistuneen_eran(self, aja_ymparisto): + tilastot = [] + with patch("arviointi.llmarviointi.kutsu.kysy", + side_effect=RuntimeError("OpenRouter-virhe (502): Upstream")): + llmarviointi.aja(TUTKIMUS, lambda n, y, e, et, ti: tilastot.append(dict(ti))) + assert tilastot[-1]["virhe_502"] == 2 # 2 kurssia yhdessä erässä + assert tilastot[-1]["onnistuneet"] == 0 + + def test_keskeyta_cb_lopettaa_ennen_eria(self, aja_ymparisto): + with patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS) as mock_kysy: + arvioitu = llmarviointi.aja(TUTKIMUS, keskeyta_cb=lambda: True) + assert arvioitu == 0 + mock_kysy.assert_not_called() + + KYSYMYKSET_MONITYYPPI = [ {"KysID": 10, "TID": 1, "Kysymys": "Kuvaile kurssia.", "Luokittelu": "vapaa_teksti", "LuokitteluMaarittely": None}, From c8efde98bfedb9224d1efa3889fc241b75bad891 Mon Sep 17 00:00:00 2001 From: "Teemu J. Tokola" Date: Tue, 14 Jul 2026 09:31:39 +0300 Subject: [PATCH 7/8] Kielioppikorjauksia... --- arviointi/llmarviointi.py | 4 +-- kehotteet => kehoteraportti | 0 .../arviointijarjestelma.txt | 0 .../luokittelujarjestelma.txt | 0 .../raporttijarjestelma.txt | 0 llm/ARVIOINTIPROTOKOLLA.md | 2 +- llm/{kehoteet.py => kehotteet.py} | 4 +-- loki.log | 28 +++++++++++++++++++ luokittelu/llmluokittelu.py | 4 +-- raportti/llmraportti.py | 2 +- webui/Dockerfile | 2 +- webui/palvelin.py | 4 +-- 12 files changed, 39 insertions(+), 11 deletions(-) rename kehotteet => kehoteraportti (100%) rename kehoteet/arviointi_jarjestelma.txt => kehotteet/arviointijarjestelma.txt (100%) rename kehoteet/luokittelu_jarjestelma.txt => kehotteet/luokittelujarjestelma.txt (100%) rename kehoteet/raportti_jarjestelma.txt => kehotteet/raporttijarjestelma.txt (100%) rename llm/{kehoteet.py => kehotteet.py} (74%) create mode 100644 loki.log diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index c4f1b59..2594a0f 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -2,7 +2,7 @@ import json import time from tietokanta import mallit -from llm import kutsu, tiiviste, kehoteet, kurssimuoto, asetukset +from llm import kutsu, tiiviste, kehotteet, kurssimuoto, asetukset _OLETUS_ERAKOKO = 5 # kursseja per LLM-kutsu; .env:n ARVIOINTI_ERAKOKO ohittaa @@ -52,7 +52,7 @@ def _luokittele_virhe(e: Exception) -> str: def _lue_jarjestelma_kehote() -> str: - return kehoteet.lue("arviointi_jarjestelma.txt") + return kehotteet.lue("arviointijarjestelma.txt") def _erittele_json(teksti: str) -> list[dict]: diff --git a/kehotteet b/kehoteraportti similarity index 100% rename from kehotteet rename to kehoteraportti diff --git a/kehoteet/arviointi_jarjestelma.txt b/kehotteet/arviointijarjestelma.txt similarity index 100% rename from kehoteet/arviointi_jarjestelma.txt rename to kehotteet/arviointijarjestelma.txt diff --git a/kehoteet/luokittelu_jarjestelma.txt b/kehotteet/luokittelujarjestelma.txt similarity index 100% rename from kehoteet/luokittelu_jarjestelma.txt rename to kehotteet/luokittelujarjestelma.txt diff --git a/kehoteet/raportti_jarjestelma.txt b/kehotteet/raporttijarjestelma.txt similarity index 100% rename from kehoteet/raportti_jarjestelma.txt rename to kehotteet/raporttijarjestelma.txt diff --git a/llm/ARVIOINTIPROTOKOLLA.md b/llm/ARVIOINTIPROTOKOLLA.md index 3568ff2..ffc1d42 100644 --- a/llm/ARVIOINTIPROTOKOLLA.md +++ b/llm/ARVIOINTIPROTOKOLLA.md @@ -14,7 +14,7 @@ LLM:lle lähetetään yksi pyyntö per erä kursseja. Viesti koostuu neljästä ### 1. Järjestelmäkehote Vakioteksti, joka asettaa LLM:n rooliin ja määrittelee vastausformaatin (JSON). -Tallennetaan tiedostoon `kehoteet/jarjestelma.txt`. +Tallennetaan tiedostoon `kehotteet/jarjestelma.txt`. ### 2. Arviointikehote diff --git a/llm/kehoteet.py b/llm/kehotteet.py similarity index 74% rename from llm/kehoteet.py rename to llm/kehotteet.py index 3688c1d..d4ed712 100644 --- a/llm/kehoteet.py +++ b/llm/kehotteet.py @@ -1,11 +1,11 @@ -"""Järjestelmäkehotteiden lukeminen kehoteet/-kansiosta. +"""Järjestelmäkehotteiden lukeminen kehotteet/-kansiosta. Jaettu lukija, jotta sekä pipeline (luokittelu/arviointi) että WebUI voivat laskea kehotetiivisteet samasta lähteestä ilman koodin kahdentamista. """ import os -_KANSIO = os.path.join(os.path.dirname(__file__), "..", "kehoteet") +_KANSIO = os.path.join(os.path.dirname(__file__), "..", "kehotteet") def lue(tiedosto: str) -> str: diff --git a/loki.log b/loki.log new file mode 100644 index 0000000..b71ab66 --- /dev/null +++ b/loki.log @@ -0,0 +1,28 @@ +2026-07-13 06:50:55 [arviointi] erä 1/30 epäonnistui (JSONDecodeError: Unterminated string starting at: line 13 column 24 (char 266)); kurssit [498, 31670, 17142, 9118, 56961] jäävät seuraavalle ajolle +2026-07-13 06:52:40 [arviointi] erä 2/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 19 column 1 (char 745)); kurssit [9113, 21096, 47700, 35600, 31793] jäävät seuraavalle ajolle +2026-07-13 06:53:50 [arviointi] erä 3/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [12208, 9102, 17120, 16874, 52919] jäävät seuraavalle ajolle +2026-07-13 06:59:32 [arviointi] erä 5/30 epäonnistui (JSONDecodeError: Invalid control character at: line 9 column 243 (char 985)); kurssit [9136, 9148, 9146, 44542, 44540] jäävät seuraavalle ajolle +2026-07-13 07:00:19 [arviointi] erä 6/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [41707, 44541, 41709, 41708, 54759] jäävät seuraavalle ajolle +2026-07-13 07:03:35 [arviointi] erä 7/30 epäonnistui (JSONDecodeError: Invalid control character at: line 92 column 12 (char 4179)); kurssit [31664, 41704, 52915, 35713, 33869] jäävät seuraavalle ajolle +2026-07-13 07:04:30 [arviointi] erä 8/30 epäonnistui (JSONDecodeError: Invalid control character at: line 7 column 30 (char 103)); kurssit [9139, 52914, 39068, 33994, 52911] jäävät seuraavalle ajolle +2026-07-13 07:04:52 [arviointi] erä 9/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: unexpected tokens remaining in message header: Some("Need to produce 5 results. Provide each answer per question. Use info. Let's")); kurssit [54240, 31650, 31218, 31384, 31677] jäävät seuraavalle ajolle +2026-07-13 07:06:51 [arviointi] erä 10/30 epäonnistui (JSONDecodeError: Invalid control character at: line 82 column 26 (char 2058)); kurssit [52913, 34139, 9169, 9167, 9162] jäävät seuraavalle ajolle +2026-07-13 07:08:16 [arviointi] erä 11/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 25 column 10 (char 1083)); kurssit [35812, 34126, 31499, 34, 54241] jäävät seuraavalle ajolle +2026-07-13 07:12:36 [arviointi] erä 13/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: unexpected tokens remaining in message header: Some("{ \"tt [{ \"\"]\"] {\"t - \"} \"} {\"t \"], \"The\"] \"} {\" {\"t 0 \"} {\" {\"t [ \"] [ \",\" {\"t [ {\"t {\" {\"t { [ { - { [ { \",\" \"} \"} [ { \"} { { [[ { [ { [ [ { [")); kurssit [9209, 12028, 39129, 56540, 51] jäävät seuraavalle ajolle +2026-07-13 07:13:32 [arviointi] erä 14/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [31733, 56541, 9189, 34499, 35980] jäävät seuraavalle ajolle +2026-07-13 07:14:35 [arviointi] erä 15/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: error)); kurssit [2092, 461, 9237, 9239, 9246] jäävät seuraavalle ajolle +2026-07-13 07:16:09 [arviointi] erä 16/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 33 column 10 (char 1298)); kurssit [9233, 1, 9241, 9243, 9242] jäävät seuraavalle ajolle +2026-07-13 07:16:42 [arviointi] erä 17/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9244, 34428, 463, 464, 52870] jäävät seuraavalle ajolle +2026-07-13 07:18:43 [arviointi] erä 18/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9245, 549, 494, 556, 539] jäävät seuraavalle ajolle +2026-07-13 07:20:21 [arviointi] erä 19/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [542, 34508, 34504, 12149, 46924] jäävät seuraavalle ajolle +2026-07-13 07:22:14 [arviointi] erä 20/30 epäonnistui (JSONDecodeError: Unterminated string starting at: line 1 column 1292 (char 1291)); kurssit [35603, 11839, 444, 32219, 44587] jäävät seuraavalle ajolle +2026-07-13 07:23:53 [arviointi] erä 21/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [52918, 41, 34802, 52886, 9290] jäävät seuraavalle ajolle +2026-07-13 07:24:39 [arviointi] erä 22/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9321, 9327, 52917, 56440, 551] jäävät seuraavalle ajolle +2026-07-13 07:26:07 [arviointi] erä 23/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [3204, 35027, 9323, 52912, 12155] jäävät seuraavalle ajolle +2026-07-13 07:26:31 [arviointi] erä 24/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 11 column 32 (char 220)); kurssit [35067, 52934, 52923, 44571, 35129] jäävät seuraavalle ajolle +2026-07-13 07:27:13 [arviointi] erä 25/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [12135, 476, 52920, 35216, 52910] jäävät seuraavalle ajolle +2026-07-13 07:28:19 [arviointi] erä 26/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [47113, 9351, 41726, 41730, 81] jäävät seuraavalle ajolle +2026-07-13 07:29:24 [arviointi] erä 27/30 epäonnistui (JSONDecodeError: Invalid control character at: line 17 column 95 (char 554)); kurssit [36412, 41758, 41784, 490, 557] jäävät seuraavalle ajolle +2026-07-13 07:30:10 [arviointi] erä 28/30 epäonnistui (JSONDecodeError: Invalid control character at: line 1 column 880 (char 879)); kurssit [41782, 76, 472, 469, 470] jäävät seuraavalle ajolle +2026-07-13 07:30:35 [arviointi] erä 29/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [9410, 9008, 52875, 9430, 12075] jäävät seuraavalle ajolle +2026-07-13 07:31:26 [arviointi] erä 30/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: channel marker present but no channel value found in header); kurssit [53] jäävät seuraavalle ajolle diff --git a/luokittelu/llmluokittelu.py b/luokittelu/llmluokittelu.py index 160df58..546fcfd 100644 --- a/luokittelu/llmluokittelu.py +++ b/luokittelu/llmluokittelu.py @@ -2,7 +2,7 @@ import json from concurrent.futures import ThreadPoolExecutor, as_completed from tietokanta import mallit -from llm import kutsu, tiiviste, kehoteet, kurssimuoto, asetukset +from llm import kutsu, tiiviste, kehotteet, kurssimuoto, asetukset _OLETUS_ERAKOKO = 20 # kursseja per LLM-kutsu; .env:n LUOKITTELU_ERAKOKO ohittaa _OLETUS_RINNAKKAISUUS = 5 # rinnakkaisia LLM-kutsuja; .env:n LLM_RINNAKKAISUUS ohittaa @@ -19,7 +19,7 @@ def rinnakkaisuus() -> int: def _lue_jarjestelma_kehote() -> str: - return kehoteet.lue("luokittelu_jarjestelma.txt") + return kehotteet.lue("luokittelujarjestelma.txt") def _erittele_json(teksti: str) -> list[dict]: diff --git a/raportti/llmraportti.py b/raportti/llmraportti.py index cafdccd..9658561 100644 --- a/raportti/llmraportti.py +++ b/raportti/llmraportti.py @@ -7,7 +7,7 @@ def _lue_jarjestelma_kehote() -> str: - polku = os.path.join(os.path.dirname(__file__), "..", "kehoteet", "raportti_jarjestelma.txt") + polku = os.path.join(os.path.dirname(__file__), "..", "kehotteet", "raporttijarjestelma.txt") with open(polku, encoding="utf-8") as f: return f.read().strip() diff --git a/webui/Dockerfile b/webui/Dockerfile index a62b4da..f3a4221 100644 --- a/webui/Dockerfile +++ b/webui/Dockerfile @@ -8,7 +8,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY tietokanta/ tietokanta/ COPY luokittelu/ luokittelu/ COPY llm/ llm/ -COPY kehoteet/ kehoteet/ +COPY kehotteet/ kehotteet/ COPY webui/ webui/ EXPOSE 12121 diff --git a/webui/palvelin.py b/webui/palvelin.py index d5edc97..d8407ff 100644 --- a/webui/palvelin.py +++ b/webui/palvelin.py @@ -15,7 +15,7 @@ from tietokanta import mallit from tietokanta.valimuisti import ttl_valimuisti -from llm import tiiviste, kehoteet +from llm import tiiviste, kehotteet _AUTH_EVASTE = "opserver_auth" @@ -441,7 +441,7 @@ def api_tutkimus_arvioinnit(slug: str) -> dict: # Nykyiset kysymystiivisteet: tunnistavat vastaukset jotka on generoitu # vanhentuneeseen kysymykseen/kehotteeseen (ennen seuraavaa LLM-ajoa). - jarjestelma = kehoteet.lue("arviointi_jarjestelma.txt") + jarjestelma = kehotteet.lue("arviointijarjestelma.txt") nyky_tiiviste = tiiviste.kysymystiivisteet( tutkimus.get("Arviointikehote") or "", jarjestelma, kysymykset ) From afea718924db57e2fc7811284f28cfaa5553992b Mon Sep 17 00:00:00 2001 From: Klaude Date: Tue, 14 Jul 2026 09:36:03 +0300 Subject: [PATCH 8/8] =?UTF-8?q?Kielioppi:=20=5Flue=5Fjarjestelma=5Fkehote?= =?UTF-8?q?=20=E2=86=92=20=5Flue=5Fjarjestelmakehote=20+=20loki-hygienia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kaksi jälkikorjausta Teemun kielioppikorjaus-mergen päälle: 1. Rename _lue_jarjestelma_kehote → _lue_jarjestelmakehote (jarjestelma_kehote → jarjestelmakehote) yhdenmukaisesti 9 tiedostossa. 2. loki.log pois versionhallinnasta: ajonaikainen loki aiheutti merge- konflikteja koneiden välillä ja likaantui joka ajolla. Gitignoretaan + poistetaan seurannasta (paikallinen tiedosto säilyy). Autouse-fixture mockaa _kirjaa → yksikkötestit eivät enää luo loki.log:ia repo-juureen. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + arviointi/llmarviointi.py | 6 +++--- loki.log | 28 ---------------------------- luokittelu/llmluokittelu.py | 6 +++--- luokittelu/testierat.py | 2 +- raportti/_kehotteet.py | 2 +- raportti/llmraportti.py | 4 ++-- testit/test_arviointi.py | 31 +++++++++++++++++++------------ testit/test_luokittelu.py | 18 +++++++++--------- testit/test_raportti.py | 8 ++++---- testit/test_testierat.py | 8 ++++---- 11 files changed, 47 insertions(+), 67 deletions(-) delete mode 100644 loki.log diff --git a/.gitignore b/.gitignore index 32fb050..c9fe015 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ Thumbs.db # Kehitysaikaiset artefaktit status +loki.log # Testierä-tilastot (generoituja) testitulokset/ diff --git a/arviointi/llmarviointi.py b/arviointi/llmarviointi.py index 2594a0f..730fa0a 100644 --- a/arviointi/llmarviointi.py +++ b/arviointi/llmarviointi.py @@ -51,7 +51,7 @@ def _luokittele_virhe(e: Exception) -> str: return "muoto" -def _lue_jarjestelma_kehote() -> str: +def _lue_jarjestelmakehote() -> str: return kehotteet.lue("arviointijarjestelma.txt") @@ -170,7 +170,7 @@ def _selvita_tyo(tutkimus: dict) -> dict: return {"kysymykset": [], "jarjestelma": "", "kys_tiiviste": {}, "arviointikehote": arviointikehote, "kurssi_kartta": {}, "olemassa": {}, "tyo": {}} - jarjestelma = _lue_jarjestelma_kehote() + jarjestelma = _lue_jarjestelmakehote() kys_tiiviste = tiiviste.kysymystiivisteet(arviointikehote, jarjestelma, kysymykset) kurssit = mallit.hae_valitut_kurssit(tid) olemassa = mallit.hae_vastaus_tiivisteet(tid) @@ -198,7 +198,7 @@ def _kysymystiivisteet(tutkimus: dict) -> dict: if not kysymykset: return {} return tiiviste.kysymystiivisteet( - tutkimus["Arviointikehote"], _lue_jarjestelma_kehote(), kysymykset) + tutkimus["Arviointikehote"], _lue_jarjestelmakehote(), kysymykset) def laske_tyomaara(tutkimus: dict, tieto: dict | None = None) -> tuple[int, int, int]: diff --git a/loki.log b/loki.log deleted file mode 100644 index b71ab66..0000000 --- a/loki.log +++ /dev/null @@ -1,28 +0,0 @@ -2026-07-13 06:50:55 [arviointi] erä 1/30 epäonnistui (JSONDecodeError: Unterminated string starting at: line 13 column 24 (char 266)); kurssit [498, 31670, 17142, 9118, 56961] jäävät seuraavalle ajolle -2026-07-13 06:52:40 [arviointi] erä 2/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 19 column 1 (char 745)); kurssit [9113, 21096, 47700, 35600, 31793] jäävät seuraavalle ajolle -2026-07-13 06:53:50 [arviointi] erä 3/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [12208, 9102, 17120, 16874, 52919] jäävät seuraavalle ajolle -2026-07-13 06:59:32 [arviointi] erä 5/30 epäonnistui (JSONDecodeError: Invalid control character at: line 9 column 243 (char 985)); kurssit [9136, 9148, 9146, 44542, 44540] jäävät seuraavalle ajolle -2026-07-13 07:00:19 [arviointi] erä 6/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [41707, 44541, 41709, 41708, 54759] jäävät seuraavalle ajolle -2026-07-13 07:03:35 [arviointi] erä 7/30 epäonnistui (JSONDecodeError: Invalid control character at: line 92 column 12 (char 4179)); kurssit [31664, 41704, 52915, 35713, 33869] jäävät seuraavalle ajolle -2026-07-13 07:04:30 [arviointi] erä 8/30 epäonnistui (JSONDecodeError: Invalid control character at: line 7 column 30 (char 103)); kurssit [9139, 52914, 39068, 33994, 52911] jäävät seuraavalle ajolle -2026-07-13 07:04:52 [arviointi] erä 9/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: unexpected tokens remaining in message header: Some("Need to produce 5 results. Provide each answer per question. Use info. Let's")); kurssit [54240, 31650, 31218, 31384, 31677] jäävät seuraavalle ajolle -2026-07-13 07:06:51 [arviointi] erä 10/30 epäonnistui (JSONDecodeError: Invalid control character at: line 82 column 26 (char 2058)); kurssit [52913, 34139, 9169, 9167, 9162] jäävät seuraavalle ajolle -2026-07-13 07:08:16 [arviointi] erä 11/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 25 column 10 (char 1083)); kurssit [35812, 34126, 31499, 34, 54241] jäävät seuraavalle ajolle -2026-07-13 07:12:36 [arviointi] erä 13/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: unexpected tokens remaining in message header: Some("{ \"tt [{ \"\"]\"] {\"t - \"} \"} {\"t \"], \"The\"] \"} {\" {\"t 0 \"} {\" {\"t [ \"] [ \",\" {\"t [ {\"t {\" {\"t { [ { - { [ { \",\" \"} \"} [ { \"} { { [[ { [ { [ [ { [")); kurssit [9209, 12028, 39129, 56540, 51] jäävät seuraavalle ajolle -2026-07-13 07:13:32 [arviointi] erä 14/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [31733, 56541, 9189, 34499, 35980] jäävät seuraavalle ajolle -2026-07-13 07:14:35 [arviointi] erä 15/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: error)); kurssit [2092, 461, 9237, 9239, 9246] jäävät seuraavalle ajolle -2026-07-13 07:16:09 [arviointi] erä 16/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 33 column 10 (char 1298)); kurssit [9233, 1, 9241, 9243, 9242] jäävät seuraavalle ajolle -2026-07-13 07:16:42 [arviointi] erä 17/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9244, 34428, 463, 464, 52870] jäävät seuraavalle ajolle -2026-07-13 07:18:43 [arviointi] erä 18/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9245, 549, 494, 556, 539] jäävät seuraavalle ajolle -2026-07-13 07:20:21 [arviointi] erä 19/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [542, 34508, 34504, 12149, 46924] jäävät seuraavalle ajolle -2026-07-13 07:22:14 [arviointi] erä 20/30 epäonnistui (JSONDecodeError: Unterminated string starting at: line 1 column 1292 (char 1291)); kurssit [35603, 11839, 444, 32219, 44587] jäävät seuraavalle ajolle -2026-07-13 07:23:53 [arviointi] erä 21/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [52918, 41, 34802, 52886, 9290] jäävät seuraavalle ajolle -2026-07-13 07:24:39 [arviointi] erä 22/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [9321, 9327, 52917, 56440, 551] jäävät seuraavalle ajolle -2026-07-13 07:26:07 [arviointi] erä 23/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [3204, 35027, 9323, 52912, 12155] jäävät seuraavalle ajolle -2026-07-13 07:26:31 [arviointi] erä 24/30 epäonnistui (JSONDecodeError: Expecting ',' delimiter: line 11 column 32 (char 220)); kurssit [35067, 52934, 52923, 44571, 35129] jäävät seuraavalle ajolle -2026-07-13 07:27:13 [arviointi] erä 25/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [12135, 476, 52920, 35216, 52910] jäävät seuraavalle ajolle -2026-07-13 07:28:19 [arviointi] erä 26/30 epäonnistui (JSONDecodeError: Expecting ':' delimiter: line 1 column 16 (char 15)); kurssit [47113, 9351, 41726, 41730, 81] jäävät seuraavalle ajolle -2026-07-13 07:29:24 [arviointi] erä 27/30 epäonnistui (JSONDecodeError: Invalid control character at: line 17 column 95 (char 554)); kurssit [36412, 41758, 41784, 490, 557] jäävät seuraavalle ajolle -2026-07-13 07:30:10 [arviointi] erä 28/30 epäonnistui (JSONDecodeError: Invalid control character at: line 1 column 880 (char 879)); kurssit [41782, 76, 472, 469, 470] jäävät seuraavalle ajolle -2026-07-13 07:30:35 [arviointi] erä 29/30 epäonnistui (ValueError: LLM palautti tyhjän vastauksen (finish_reason: stop)); kurssit [9410, 9008, 52875, 9430, 12075] jäävät seuraavalle ajolle -2026-07-13 07:31:26 [arviointi] erä 30/30 epäonnistui (RuntimeError: OpenRouter-virhe (502): Upstream error from OpenInference: channel marker present but no channel value found in header); kurssit [53] jäävät seuraavalle ajolle diff --git a/luokittelu/llmluokittelu.py b/luokittelu/llmluokittelu.py index 546fcfd..db4ea56 100644 --- a/luokittelu/llmluokittelu.py +++ b/luokittelu/llmluokittelu.py @@ -18,7 +18,7 @@ def rinnakkaisuus() -> int: return max(1, asetukset.lue_int("LLM_RINNAKKAISUUS", _OLETUS_RINNAKKAISUUS)) -def _lue_jarjestelma_kehote() -> str: +def _lue_jarjestelmakehote() -> str: return kehotteet.lue("luokittelujarjestelma.txt") @@ -52,7 +52,7 @@ def _luokittele_erä(erä: list[dict], luokittelukehote: str, jarjestelma: str) def laske_tiiviste(tutkimus: dict) -> str: """Nykyisen luokittelukehotteen tiiviste (kehote + järjestelmäkehote).""" - return tiiviste.luokittelu(tutkimus["Luokittelukehote"], _lue_jarjestelma_kehote()) + return tiiviste.luokittelu(tutkimus["Luokittelukehote"], _lue_jarjestelmakehote()) def aja(tutkimus: dict, edistyminen_cb=None) -> tuple[int, int, int]: @@ -71,7 +71,7 @@ def aja(tutkimus: dict, edistyminen_cb=None) -> tuple[int, int, int]: """ tid = tutkimus["TID"] luokittelukehote = tutkimus["Luokittelukehote"] - jarjestelma = _lue_jarjestelma_kehote() + jarjestelma = _lue_jarjestelmakehote() tiiv = tiiviste.luokittelu(luokittelukehote, jarjestelma) # Tiiviste mukana → ajaa myös vanhentuneen kehotteen tulokset uudelleen, diff --git a/luokittelu/testierat.py b/luokittelu/testierat.py index eff7f31..cd21f14 100644 --- a/luokittelu/testierat.py +++ b/luokittelu/testierat.py @@ -88,7 +88,7 @@ def aja_testierat(tutkimus: dict, erakoko: int, montako_era: int, polku = tilastopolku or TILASTOPOLKU tid = tutkimus["TID"] luokittelukehote = tutkimus["Luokittelukehote"] - jarjestelma = llmluokittelu._lue_jarjestelma_kehote() + jarjestelma = llmluokittelu._lue_jarjestelmakehote() tiiv = tiiviste.luokittelu(luokittelukehote, jarjestelma) malli = kutsu.hae_malli() diff --git a/raportti/_kehotteet.py b/raportti/_kehotteet.py index a1dfd39..785ad2e 100644 --- a/raportti/_kehotteet.py +++ b/raportti/_kehotteet.py @@ -30,7 +30,7 @@ tid = tutkimus["TID"] tilastot = mallit.hae_tilastot_yliopistoittain(tid) kysymykset = mallit.hae_kysymykset(tid) -jarjestelma = llmraportti._lue_jarjestelma_kehote() +jarjestelma = llmraportti._lue_jarjestelmakehote() viestirakentajat = { "johdanto": lambda: llmraportti._rakenna_johdanto_viesti(tutkimus, tilastot), diff --git a/raportti/llmraportti.py b/raportti/llmraportti.py index 9658561..7b14426 100644 --- a/raportti/llmraportti.py +++ b/raportti/llmraportti.py @@ -6,7 +6,7 @@ OSIOT = ["johdanto", "kurssit", "arvioinnit"] -def _lue_jarjestelma_kehote() -> str: +def _lue_jarjestelmakehote() -> str: polku = os.path.join(os.path.dirname(__file__), "..", "kehotteet", "raporttijarjestelma.txt") with open(polku, encoding="utf-8") as f: return f.read().strip() @@ -104,7 +104,7 @@ def aja(tutkimus: dict, edistyminen_cb=None) -> int: Palauttaa generoitujen osioiden määrän. Idempotentti — korvaa olemassa olevat. """ tid = tutkimus["TID"] - jarjestelma = _lue_jarjestelma_kehote() + jarjestelma = _lue_jarjestelmakehote() tilastot = mallit.hae_tilastot_yliopistoittain(tid) kysymykset = mallit.hae_kysymykset(tid) diff --git a/testit/test_arviointi.py b/testit/test_arviointi.py index a762487..61ef39c 100644 --- a/testit/test_arviointi.py +++ b/testit/test_arviointi.py @@ -23,6 +23,13 @@ TUTKIMUS = {"TID": 1, "LuokittelunNimi": "Testi", "Arviointikehote": "Arvioi kyberturvallisuusrelevanssi."} +@pytest.fixture(autouse=True) +def _ei_kirjoita_lokia(): + """Estää testejä kirjoittamasta loki.log:ia työhakemistoon (aja() kirjaa siihen).""" + with patch("arviointi.llmarviointi._kirjaa"): + yield + + class TestErittelleJson: def test_erittele_json_puhdas(self): tulos = llmarviointi._erittele_json(LLM_VASTAUS) @@ -57,7 +64,7 @@ def test_aja_kirjoittaa_vastaukset(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS), \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="testimalli"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus") as mock_aseta, \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): arvioitu = llmarviointi.aja(TUTKIMUS) assert arvioitu == 2 @@ -81,7 +88,7 @@ def test_aja_ohittaa_jo_arvioidut_samalla_tiivisteella(self): patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value=olemassa), \ patch("arviointi.llmarviointi.kutsu.kysy") as mock_kysy, \ patch("arviointi.llmarviointi.mallit.aseta_vastaus") as mock_aseta, \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value=jarj): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value=jarj): arvioitu = llmarviointi.aja(TUTKIMUS) assert arvioitu == 0 mock_kysy.assert_not_called() @@ -103,7 +110,7 @@ def test_aja_kysyy_vain_muuttuneen_kysymyksen(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=vastaus) as mock_kysy, \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus") as mock_aseta, \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value=jarj): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value=jarj): llmarviointi.aja(TUTKIMUS) viesti = mock_kysy.call_args.args[0] assert "Soveltuuko" in viesti # kysymys 11 mukana @@ -122,7 +129,7 @@ def test_aja_ilman_valittuja_kursseja_palauttaa_nollan(self): with patch("arviointi.llmarviointi.mallit.hae_kysymykset", return_value=KYSYMYKSET), \ patch("arviointi.llmarviointi.mallit.hae_valitut_kurssit", return_value=[]), \ patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value={}), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"), \ + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"), \ patch("arviointi.llmarviointi.kutsu.kysy") as mock_kysy: arvioitu = llmarviointi.aja(TUTKIMUS) assert arvioitu == 0 @@ -135,7 +142,7 @@ def test_aja_kutsuu_kysy_json_muodolla(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS) as mock_kysy, \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): llmarviointi.aja(TUTKIMUS) _, kwargs = mock_kysy.call_args assert kwargs.get("json_muoto") is True @@ -151,7 +158,7 @@ def edistyminen(n, yht, erä, erat, tilasto): patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS), \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): llmarviointi.aja(TUTKIMUS, edistyminen) # Pre-batch (0 käsitelty) ja post-batch (2 käsitelty) — yhteensä 2 tapahtumaa @@ -172,7 +179,7 @@ def test_aja_max_erat_rajaa_yhteen_pyyntoon(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=vastaus) as mock_kysy, \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): llmarviointi.aja(TUTKIMUS, lambda n, y, e, et, ti: tapahtumat.append(et), max_erat=1) assert mock_kysy.call_count == 1 assert all(et == 1 for et in tapahtumat) # eräkokonaismäärä näkyy rajattuna @@ -188,7 +195,7 @@ def test_aja_ilman_max_erat_ajaa_kaikki_erat(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=vastaus) as mock_kysy, \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): llmarviointi.aja(TUTKIMUS) assert mock_kysy.call_count == 2 @@ -203,7 +210,7 @@ def test_laske_tyomaara_erottaa_uudet_ja_vanhentuneet(self): with patch("arviointi.llmarviointi.mallit.hae_kysymykset", return_value=KYSYMYKSET), \ patch("arviointi.llmarviointi.mallit.hae_valitut_kurssit", return_value=KURSSIT), \ patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value=olemassa), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value=jarj): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value=jarj): uudet, taydennettavat, muuttuneet = llmarviointi.laske_tyomaara(TUTKIMUS) assert uudet == 1 # kurssi 2: ei vastauksia assert taydennettavat == 0 @@ -215,7 +222,7 @@ def test_kysymystiivisteet_ei_hae_kursseja_eika_vastauksia(self): with patch("arviointi.llmarviointi.mallit.hae_kysymykset", return_value=KYSYMYKSET), \ patch("arviointi.llmarviointi.mallit.hae_valitut_kurssit") as mock_kurssit, \ patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet") as mock_vast, \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): tiivisteet = llmarviointi._kysymystiivisteet(TUTKIMUS) assert set(tiivisteet) == {10, 11} # KysID:t assert all(len(t) == 64 for t in tiivisteet.values()) @@ -249,7 +256,7 @@ def test_aja_esilasketulla_tiedolla_ei_hae_kursseja(self): patch("arviointi.llmarviointi.kutsu.kysy", return_value=LLM_VASTAUS), \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus") as mock_aseta, \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value=jarj): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value=jarj): arvioitu = llmarviointi.aja(TUTKIMUS, tieto=tieto) assert arvioitu == 2 assert mock_aseta.call_count == 4 # 2 kurssia × 2 kysymystä @@ -287,7 +294,7 @@ def aja_ymparisto(): patch("arviointi.llmarviointi.mallit.hae_vastaus_tiivisteet", return_value={}), \ patch("arviointi.llmarviointi.kutsu.hae_malli", return_value="m"), \ patch("arviointi.llmarviointi.mallit.aseta_vastaus"), \ - patch("arviointi.llmarviointi._lue_jarjestelma_kehote", return_value="system"): + patch("arviointi.llmarviointi._lue_jarjestelmakehote", return_value="system"): yield diff --git a/testit/test_luokittelu.py b/testit/test_luokittelu.py index 4619564..e2b0454 100644 --- a/testit/test_luokittelu.py +++ b/testit/test_luokittelu.py @@ -147,7 +147,7 @@ def test_aja_kirjoittaa_luokitukset(self): side_effect=[kandidaatit, []]) as mock_hae, \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value=self.LLM_VASTAUS), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus") as mock_aseta, \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): mukana, hylätty, luokittelematta = llmluokittelu.aja(tutkimus) assert mukana == 1 assert hylätty == 1 @@ -170,7 +170,7 @@ def test_aja_tyhja_kehote_hyvaksyy_meta_ilman_llm(self): with patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", return_value=kandidaatit), \ patch("luokittelu.llmluokittelu.kutsu.kysy") as mock_kysy, \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus") as mock_aseta, \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): mukana, hylätty, luokittelematta = llmluokittelu.aja(tutkimus) assert (mukana, hylätty, luokittelematta) == (2, 0, 0) assert mock_kysy.call_count == 0 # ei LLM-kutsuja @@ -187,7 +187,7 @@ def test_aja_viallinen_era_ei_kaada_ajoa(self): with patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", return_value=kandidaatit), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value="ei kelvollista jsonia"), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus") as mock_aseta, \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): mukana, hylätty, luokittelematta = llmluokittelu.aja(tutkimus) assert (mukana, hylätty) == (0, 0) assert luokittelematta == 1 # ainoa kurssi jäi ilman päätöstä @@ -214,7 +214,7 @@ def test_aja_edistyminen_saa_juoksevat_mukana_ja_hylatty(self): side_effect=[kandidaatit, []]), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value=self.LLM_VASTAUS), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus"), \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): llmluokittelu.aja(tutkimus, edistyminen_cb=lambda *a: kutsut.append(a)) # (käsitelty, yhteensä, valmiit, erät, mukana, hylätty, epäonnistunut) viimeinen = kutsut[-1] @@ -234,7 +234,7 @@ def test_aja_edistyminen_laskee_epaonnistuneet_kurssit(self): with patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", return_value=kandidaatit), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value="ei kelvollista jsonia"), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus"), \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): llmluokittelu.aja(tutkimus, edistyminen_cb=lambda *a: kutsut.append(a)) viimeinen = kutsut[-1] assert (viimeinen[4], viimeinen[5], viimeinen[6]) == (0, 0, 1) # mukaan, hylätty, epäonnistunut @@ -251,7 +251,7 @@ def test_aja_tilasto_erottaa_menetetyn_eran(self): with patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", return_value=kandidaatit), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value="ei kelvollista jsonia"), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus"), \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): llmluokittelu.aja(tutkimus, edistyminen_cb=lambda *a: kutsut.append(a)) tilasto = kutsut[-1][7] assert tilasto == {"menetetyt_erat": 1, "menetetyt_kurssit": 1, "ilman_vastausta": 0} @@ -273,7 +273,7 @@ def test_aja_tilasto_laskee_ilman_vastausta_kurssit(self): patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", side_effect=[kandidaatit, []]), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value=vain_id1), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus"), \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): llmluokittelu.aja(tutkimus, edistyminen_cb=lambda *a: kutsut.append(a)) tilasto = kutsut[-1][7] assert tilasto == {"menetetyt_erat": 0, "menetetyt_kurssit": 0, "ilman_vastausta": 1} @@ -293,7 +293,7 @@ def test_aja_keskeytys_lopettaa_ja_sailyttaa_tehdyt(self, monkeypatch): patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", return_value=kandidaatit), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value=yksi), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus") as mock_aseta, \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): # Callback pyytää heti keskeytystä (palauttaa True ensimmäisellä kutsulla) mukana, hylätty, _ = llmluokittelu.aja(tutkimus, edistyminen_cb=lambda *a: True) assert mock_aseta.call_count == 1 # vain ensimmäinen erä ehti tallentua @@ -325,6 +325,6 @@ def vakooja(*a, max_workers=None, **k): patch("luokittelu.llmluokittelu.mallit.hae_luokittelemattomat", side_effect=[kandidaatit, []]), \ patch("luokittelu.llmluokittelu.kutsu.kysy", return_value=self.LLM_VASTAUS), \ patch("luokittelu.llmluokittelu.mallit.aseta_luokitus"), \ - patch("luokittelu.llmluokittelu._lue_jarjestelma_kehote", return_value="system"): + patch("luokittelu.llmluokittelu._lue_jarjestelmakehote", return_value="system"): llmluokittelu.aja(tutkimus) assert nahdyt["max_workers"] == 7 diff --git a/testit/test_raportti.py b/testit/test_raportti.py index 29c6a39..0aa6315 100644 --- a/testit/test_raportti.py +++ b/testit/test_raportti.py @@ -111,7 +111,7 @@ def test_aja_generoi_kaikki_osiot(self): patch("raportti.llmraportti.mallit.hae_arviokommentit_kaikki", return_value=[]), \ patch("raportti.llmraportti.mallit.aseta_raportti_osio") as mock_aseta, \ patch("raportti.llmraportti.kutsu.kysy", return_value="LLM-teksti") as mock_kysy, \ - patch("raportti.llmraportti._lue_jarjestelma_kehote", return_value="jarj"): + patch("raportti.llmraportti._lue_jarjestelmakehote", return_value="jarj"): lkm = llmraportti.aja(TUTKIMUS) assert lkm == 3 @@ -127,7 +127,7 @@ def test_aja_tallentaa_llm_tekstin(self): patch("raportti.llmraportti.mallit.hae_arviokommentit_kaikki", return_value=[]), \ patch("raportti.llmraportti.mallit.aseta_raportti_osio") as mock_aseta, \ patch("raportti.llmraportti.kutsu.kysy", return_value="Generoitu teksti"), \ - patch("raportti.llmraportti._lue_jarjestelma_kehote", return_value="jarj"): + patch("raportti.llmraportti._lue_jarjestelmakehote", return_value="jarj"): llmraportti.aja(TUTKIMUS) mock_aseta.assert_any_call(1, "johdanto", "Generoitu teksti") @@ -142,7 +142,7 @@ def edistyminen(n, yht, avain): patch("raportti.llmraportti.mallit.hae_arviokommentit_kaikki", return_value=[]), \ patch("raportti.llmraportti.mallit.aseta_raportti_osio"), \ patch("raportti.llmraportti.kutsu.kysy", return_value="teksti"), \ - patch("raportti.llmraportti._lue_jarjestelma_kehote", return_value="jarj"): + patch("raportti.llmraportti._lue_jarjestelmakehote", return_value="jarj"): llmraportti.aja(TUTKIMUS, edistyminen) # 3 osio-callbackia + 1 "valmis" @@ -157,7 +157,7 @@ def test_aja_on_idempotentti(self): patch("raportti.llmraportti.mallit.hae_arviokommentit_kaikki", return_value=[]), \ patch("raportti.llmraportti.mallit.aseta_raportti_osio") as mock_aseta, \ patch("raportti.llmraportti.kutsu.kysy", return_value="teksti"), \ - patch("raportti.llmraportti._lue_jarjestelma_kehote", return_value="jarj"): + patch("raportti.llmraportti._lue_jarjestelmakehote", return_value="jarj"): llmraportti.aja(TUTKIMUS) llmraportti.aja(TUTKIMUS) diff --git a/testit/test_testierat.py b/testit/test_testierat.py index 77ff01f..9502c03 100644 --- a/testit/test_testierat.py +++ b/testit/test_testierat.py @@ -37,7 +37,7 @@ def mockit(): """Mockaa kanta, LLM-kutsu ja kehotteet — ei verkkoa, ei tiedostoja.""" with patch("luokittelu.testierat.mallit.hae_luokittelemattomat", return_value=_kurssit(4)), \ patch("luokittelu.testierat.testimallit.aseta_testiluokitus") as aseta, \ - patch("luokittelu.testierat.llmluokittelu._lue_jarjestelma_kehote", return_value="JARJ"), \ + patch("luokittelu.testierat.llmluokittelu._lue_jarjestelmakehote", return_value="JARJ"), \ patch("luokittelu.testierat.tiiviste.luokittelu", return_value="TIIV"), \ patch("luokittelu.testierat.kutsu.hae_malli", return_value="openai/gpt-oss-120b:free"), \ patch("luokittelu.testierat.kutsu.hae_viimeisin_kaytto", @@ -90,7 +90,7 @@ def test_sama_kurssi_ei_osu_kahteen_eraan(tmp_path): """Satunnaisotos ilman takaisinpanoa: kukin kurssi enintään yhdessä erässä.""" with patch("luokittelu.testierat.mallit.hae_luokittelemattomat", return_value=_kurssit(20)), \ patch("luokittelu.testierat.testimallit.aseta_testiluokitus") as aseta, \ - patch("luokittelu.testierat.llmluokittelu._lue_jarjestelma_kehote", return_value="JARJ"), \ + patch("luokittelu.testierat.llmluokittelu._lue_jarjestelmakehote", return_value="JARJ"), \ patch("luokittelu.testierat.tiiviste.luokittelu", return_value="TIIV"), \ patch("luokittelu.testierat.kutsu.hae_malli", return_value="m"), \ patch("luokittelu.testierat.kutsu.hae_viimeisin_kaytto", return_value={"finish_reason": "stop"}), \ @@ -105,7 +105,7 @@ def test_pudonneet_kurssit_lasketaan(tmp_path): polku = tmp_path / "tilastot.jsonl" with patch("luokittelu.testierat.mallit.hae_luokittelemattomat", return_value=_kurssit(2)), \ patch("luokittelu.testierat.testimallit.aseta_testiluokitus"), \ - patch("luokittelu.testierat.llmluokittelu._lue_jarjestelma_kehote", return_value="JARJ"), \ + patch("luokittelu.testierat.llmluokittelu._lue_jarjestelmakehote", return_value="JARJ"), \ patch("luokittelu.testierat.tiiviste.luokittelu", return_value="TIIV"), \ patch("luokittelu.testierat.kutsu.hae_malli", return_value="m"), \ patch("luokittelu.testierat.kutsu.hae_viimeisin_kaytto", return_value={"finish_reason": "stop"}), \ @@ -121,7 +121,7 @@ def test_jasennys_epaonnistuu_merkitaan(tmp_path): polku = tmp_path / "tilastot.jsonl" with patch("luokittelu.testierat.mallit.hae_luokittelemattomat", return_value=_kurssit(2)), \ patch("luokittelu.testierat.testimallit.aseta_testiluokitus"), \ - patch("luokittelu.testierat.llmluokittelu._lue_jarjestelma_kehote", return_value="JARJ"), \ + patch("luokittelu.testierat.llmluokittelu._lue_jarjestelmakehote", return_value="JARJ"), \ patch("luokittelu.testierat.tiiviste.luokittelu", return_value="TIIV"), \ patch("luokittelu.testierat.kutsu.hae_malli", return_value="m"), \ patch("luokittelu.testierat.kutsu.hae_viimeisin_kaytto", return_value={"finish_reason": "length"}), \