From 0eba1470181c7b9e0a79964a1ef28c09bcbdd9d7 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 11 Dec 2025 05:37:59 -0600 Subject: [PATCH 01/12] fix: populate room_name in transcript GET endpoint (#783) Fixes https://gitea.app.monadical.io/monadical/internalai/issues/14 --- server/reflector/views/transcripts.py | 7 ++++ server/tests/test_transcripts.py | 51 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 27663dc6..02c1aa3e 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -17,6 +17,7 @@ from pydantic import ( import reflector.auth as auth from reflector.db import get_database from reflector.db.recordings import recordings_controller +from reflector.db.rooms import rooms_controller from reflector.db.search import ( DEFAULT_SEARCH_LIMIT, SearchLimit, @@ -473,6 +474,11 @@ async def transcript_get( is_multitrack = await _get_is_multitrack(transcript) + room_name = None + if transcript.room_id: + room = await rooms_controller.get_by_id(transcript.room_id) + room_name = room.name if room else None + participants = [] if transcript.participants: user_ids = [p.user_id for p in transcript.participants if p.user_id is not None] @@ -503,6 +509,7 @@ async def transcript_get( "meeting_id": transcript.meeting_id, "source_kind": transcript.source_kind, "room_id": transcript.room_id, + "room_name": room_name, "audio_deleted": transcript.audio_deleted, "participants": participants, } diff --git a/server/tests/test_transcripts.py b/server/tests/test_transcripts.py index 2c6acc77..9a3ff576 100644 --- a/server/tests/test_transcripts.py +++ b/server/tests/test_transcripts.py @@ -1,5 +1,8 @@ import pytest +from reflector.db.rooms import rooms_controller +from reflector.db.transcripts import transcripts_controller + @pytest.mark.asyncio async def test_transcript_create(client): @@ -182,3 +185,51 @@ async def test_transcript_mark_reviewed(authenticated_client, client): response = await client.get(f"/transcripts/{tid}") assert response.status_code == 200 assert response.json()["reviewed"] is True + + +@pytest.mark.asyncio +async def test_transcript_get_returns_room_name(authenticated_client, client): + """Test that getting a transcript returns its room_name when linked to a room.""" + # Create a room + room = await rooms_controller.add( + name="test-room-for-transcript", + user_id="test-user", + zulip_auto_post=False, + zulip_stream="", + zulip_topic="", + is_locked=False, + room_mode="normal", + recording_type="cloud", + recording_trigger="automatic-2nd-participant", + is_shared=False, + webhook_url="", + webhook_secret="", + ) + + # Create a transcript linked to the room + transcript = await transcripts_controller.add( + name="transcript-with-room", + source_kind="file", + room_id=room.id, + ) + + # Get the transcript and verify room_name is returned + response = await client.get(f"/transcripts/{transcript.id}") + assert response.status_code == 200 + assert response.json()["room_id"] == room.id + assert response.json()["room_name"] == "test-room-for-transcript" + + +@pytest.mark.asyncio +async def test_transcript_get_returns_null_room_name_when_no_room( + authenticated_client, client +): + """Test that room_name is null when transcript has no room.""" + response = await client.post("/transcripts", json={"name": "no-room-transcript"}) + assert response.status_code == 200 + tid = response.json()["id"] + + response = await client.get(f"/transcripts/{tid}") + assert response.status_code == 200 + assert response.json()["room_id"] is None + assert response.json()["room_name"] is None From a47a5f57813527a340eccb15c19ee4b0f8bd5125 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 11 Dec 2025 05:43:25 -0600 Subject: [PATCH 02/12] chore(main): release 0.23.1 (#784) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11abb526..32313f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.23.1](https://github.com/Monadical-SAS/reflector/compare/v0.23.0...v0.23.1) (2025-12-11) + + +### Bug Fixes + +* populate room_name in transcript GET endpoint ([#783](https://github.com/Monadical-SAS/reflector/issues/783)) ([0eba147](https://github.com/Monadical-SAS/reflector/commit/0eba1470181c7b9e0a79964a1ef28c09bcbdd9d7)) + ## [0.23.0](https://github.com/Monadical-SAS/reflector/compare/v0.22.4...v0.23.0) (2025-12-10) From d7f140b7d1f4660d5da7a0da1357f68869e0b5cd Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 11 Dec 2025 13:30:36 +0100 Subject: [PATCH 03/12] fix: build on push tags (#785) --- .github/workflows/dockerhub-backend.yml | 14 +++----------- .github/workflows/dockerhub-frontend.yml | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/.github/workflows/dockerhub-backend.yml b/.github/workflows/dockerhub-backend.yml index 7a6c418b..c2c09463 100644 --- a/.github/workflows/dockerhub-backend.yml +++ b/.github/workflows/dockerhub-backend.yml @@ -1,12 +1,9 @@ name: Build and Push Backend Docker Image (Docker Hub) on: - pull_request: - types: - - closed - paths: - - "server/**" - - ".github/workflows/dockerhub-backend.yml" + push: + tags: + - "v*" workflow_dispatch: env: @@ -16,11 +13,6 @@ env: jobs: build-and-push: runs-on: ubuntu-latest - if: | - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'release-please--branches--')) - permissions: contents: read diff --git a/.github/workflows/dockerhub-frontend.yml b/.github/workflows/dockerhub-frontend.yml index 8eba8c17..e4962691 100644 --- a/.github/workflows/dockerhub-frontend.yml +++ b/.github/workflows/dockerhub-frontend.yml @@ -1,12 +1,9 @@ name: Build and Push Frontend Docker Image on: - pull_request: - types: - - closed - paths: - - "www/**" - - ".github/workflows/dockerhub-frontend.yml" + push: + tags: + - "v*" workflow_dispatch: env: @@ -16,11 +13,6 @@ env: jobs: build-and-push: runs-on: ubuntu-latest - if: | - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'release-please--branches--')) - permissions: contents: read From 90c3ecc9c3dc47b2d0b085cecd36be6acbe38b24 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 11 Dec 2025 06:37:41 -0600 Subject: [PATCH 04/12] chore(main): release 0.23.2 (#786) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32313f85..5bc01f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.23.2](https://github.com/Monadical-SAS/reflector/compare/v0.23.1...v0.23.2) (2025-12-11) + + +### Bug Fixes + +* build on push tags ([#785](https://github.com/Monadical-SAS/reflector/issues/785)) ([d7f140b](https://github.com/Monadical-SAS/reflector/commit/d7f140b7d1f4660d5da7a0da1357f68869e0b5cd)) + ## [0.23.1](https://github.com/Monadical-SAS/reflector/compare/v0.23.0...v0.23.1) (2025-12-11) From 23023b3cdbf8525fc024d7d5768a8c26a3e4573f Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Mon, 15 Dec 2025 13:58:34 -0500 Subject: [PATCH 05/12] update nextjs (#791) Co-authored-by: Igor Loskutov --- www/package.json | 2 +- www/pnpm-lock.yaml | 444 ++++++++++++++++++++++++--------------------- 2 files changed, 238 insertions(+), 208 deletions(-) diff --git a/www/package.json b/www/package.json index 63e68014..13895a3a 100644 --- a/www/package.json +++ b/www/package.json @@ -31,7 +31,7 @@ "ioredis": "^5.7.0", "jest-worker": "^29.6.2", "lucide-react": "^0.525.0", - "next": "^15.5.7", + "next": "^15.5.9", "next-auth": "^4.24.7", "next-themes": "^0.4.6", "nuqs": "^2.4.3", diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index f33f0b6c..4cc219ea 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: version: 0.2.3(@fortawesome/fontawesome-svg-core@6.7.2)(react@18.3.1) "@sentry/nextjs": specifier: ^10.11.0 - version: 10.11.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)(webpack@5.101.3) + version: 10.11.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)(webpack@5.101.3) "@tanstack/react-query": specifier: ^5.85.9 version: 5.85.9(react@18.3.1) @@ -62,17 +62,17 @@ importers: specifier: ^0.525.0 version: 0.525.0(react@18.3.1) next: - specifier: ^15.5.7 - version: 15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + specifier: ^15.5.9 + version: 15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) next-auth: specifier: ^4.24.7 - version: 4.24.11(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.11(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) + version: 2.4.3(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) openapi-fetch: specifier: ^0.14.0 version: 0.14.0 @@ -509,6 +509,12 @@ packages: integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==, } + "@emnapi/runtime@1.7.1": + resolution: + { + integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==, + } + "@emnapi/wasi-threads@1.0.4": resolution: { @@ -758,189 +764,213 @@ packages: } engines: { node: ">=18.18" } - "@img/sharp-darwin-arm64@0.34.3": + "@img/colour@1.0.0": resolution: { - integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==, + integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==, + } + engines: { node: ">=18" } + + "@img/sharp-darwin-arm64@0.34.5": + resolution: + { + integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [darwin] - "@img/sharp-darwin-x64@0.34.3": + "@img/sharp-darwin-x64@0.34.5": resolution: { - integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==, + integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [darwin] - "@img/sharp-libvips-darwin-arm64@1.2.0": + "@img/sharp-libvips-darwin-arm64@1.2.4": resolution: { - integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==, + integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, } cpu: [arm64] os: [darwin] - "@img/sharp-libvips-darwin-x64@1.2.0": + "@img/sharp-libvips-darwin-x64@1.2.4": resolution: { - integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==, + integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, } cpu: [x64] os: [darwin] - "@img/sharp-libvips-linux-arm64@1.2.0": + "@img/sharp-libvips-linux-arm64@1.2.4": resolution: { - integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==, + integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, } cpu: [arm64] os: [linux] - "@img/sharp-libvips-linux-arm@1.2.0": + "@img/sharp-libvips-linux-arm@1.2.4": resolution: { - integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==, + integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, } cpu: [arm] os: [linux] - "@img/sharp-libvips-linux-ppc64@1.2.0": + "@img/sharp-libvips-linux-ppc64@1.2.4": resolution: { - integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==, + integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, } cpu: [ppc64] os: [linux] - "@img/sharp-libvips-linux-s390x@1.2.0": + "@img/sharp-libvips-linux-riscv64@1.2.4": resolution: { - integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==, + integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, + } + cpu: [riscv64] + os: [linux] + + "@img/sharp-libvips-linux-s390x@1.2.4": + resolution: + { + integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, } cpu: [s390x] os: [linux] - "@img/sharp-libvips-linux-x64@1.2.0": + "@img/sharp-libvips-linux-x64@1.2.4": resolution: { - integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==, + integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, } cpu: [x64] os: [linux] - "@img/sharp-libvips-linuxmusl-arm64@1.2.0": + "@img/sharp-libvips-linuxmusl-arm64@1.2.4": resolution: { - integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==, + integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, } cpu: [arm64] os: [linux] - "@img/sharp-libvips-linuxmusl-x64@1.2.0": + "@img/sharp-libvips-linuxmusl-x64@1.2.4": resolution: { - integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==, + integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, } cpu: [x64] os: [linux] - "@img/sharp-linux-arm64@0.34.3": + "@img/sharp-linux-arm64@0.34.5": resolution: { - integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==, + integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] - "@img/sharp-linux-arm@0.34.3": + "@img/sharp-linux-arm@0.34.5": resolution: { - integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==, + integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm] os: [linux] - "@img/sharp-linux-ppc64@0.34.3": + "@img/sharp-linux-ppc64@0.34.5": resolution: { - integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==, + integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [ppc64] os: [linux] - "@img/sharp-linux-s390x@0.34.3": + "@img/sharp-linux-riscv64@0.34.5": resolution: { - integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==, + integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [riscv64] + os: [linux] + + "@img/sharp-linux-s390x@0.34.5": + resolution: + { + integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [s390x] os: [linux] - "@img/sharp-linux-x64@0.34.3": + "@img/sharp-linux-x64@0.34.5": resolution: { - integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==, + integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] - "@img/sharp-linuxmusl-arm64@0.34.3": + "@img/sharp-linuxmusl-arm64@0.34.5": resolution: { - integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==, + integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] - "@img/sharp-linuxmusl-x64@0.34.3": + "@img/sharp-linuxmusl-x64@0.34.5": resolution: { - integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==, + integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] - "@img/sharp-wasm32@0.34.3": + "@img/sharp-wasm32@0.34.5": resolution: { - integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==, + integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [wasm32] - "@img/sharp-win32-arm64@0.34.3": + "@img/sharp-win32-arm64@0.34.5": resolution: { - integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==, + integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [win32] - "@img/sharp-win32-ia32@0.34.3": + "@img/sharp-win32-ia32@0.34.5": resolution: { - integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==, + integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [ia32] os: [win32] - "@img/sharp-win32-x64@0.34.3": + "@img/sharp-win32-x64@0.34.5": resolution: { - integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==, + integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] @@ -1184,10 +1214,10 @@ packages: integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==, } - "@next/env@15.5.7": + "@next/env@15.5.9": resolution: { - integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==, + integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==, } "@next/eslint-plugin-next@15.5.3": @@ -2610,10 +2640,10 @@ packages: peerDependencies: react: ^18 || ^19 - "@tsconfig/node10@1.0.11": + "@tsconfig/node10@1.0.12": resolution: { - integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==, + integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==, } "@tsconfig/node12@1.0.11": @@ -2785,10 +2815,10 @@ packages: integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==, } - "@types/node@24.3.1": + "@types/node@25.0.2": resolution: { - integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==, + integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==, } "@types/parse-json@4.0.2": @@ -4202,6 +4232,12 @@ packages: integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==, } + caniuse-lite@1.0.30001760: + resolution: + { + integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==, + } + ccount@2.0.1: resolution: { @@ -4371,19 +4407,6 @@ packages: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, } - color-string@1.9.1: - resolution: - { - integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==, - } - - color@4.2.3: - resolution: - { - integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==, - } - engines: { node: ">=12.5.0" } - colorette@1.4.0: resolution: { @@ -4622,10 +4645,10 @@ packages: engines: { node: ">=0.10" } hasBin: true - detect-libc@2.0.4: + detect-libc@2.1.2: resolution: { - integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==, + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, } engines: { node: ">=8" } @@ -4750,10 +4773,10 @@ packages: } engines: { node: ">=10.0.0" } - enhanced-resolve@5.18.3: + enhanced-resolve@5.18.4: resolution: { - integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==, + integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==, } engines: { node: ">=10.13.0" } @@ -5711,12 +5734,6 @@ packages: integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, } - is-arrayish@0.3.2: - resolution: - { - integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==, - } - is-async-function@2.1.1: resolution: { @@ -6392,10 +6409,10 @@ packages: integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, } - loader-runner@4.3.0: + loader-runner@4.3.1: resolution: { - integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==, + integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==, } engines: { node: ">=6.11.5" } @@ -6863,10 +6880,10 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.5.7: + next@15.5.9: resolution: { - integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==, + integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==, } engines: { node: ^18.18.0 || ^19.8.0 || >= 20.0.0 } hasBin: true @@ -7870,10 +7887,10 @@ packages: integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, } - schema-utils@4.3.2: + schema-utils@4.3.3: resolution: { - integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==, + integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==, } engines: { node: ">= 10.13.0" } @@ -7905,6 +7922,14 @@ packages: engines: { node: ">=10" } hasBin: true + semver@7.7.3: + resolution: + { + integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, + } + engines: { node: ">=10" } + hasBin: true + serialize-javascript@6.0.2: resolution: { @@ -7932,10 +7957,10 @@ packages: } engines: { node: ">= 0.4" } - sharp@0.34.3: + sharp@0.34.5: resolution: { - integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==, + integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, } engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } @@ -8006,12 +8031,6 @@ packages: integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==, } - simple-swizzle@0.2.2: - resolution: - { - integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==, - } - slash@3.0.0: resolution: { @@ -8325,17 +8344,17 @@ packages: engines: { node: ">=14.0.0" } hasBin: true - tapable@2.2.3: + tapable@2.3.0: resolution: { - integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==, + integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==, } engines: { node: ">=6" } - terser-webpack-plugin@5.3.14: + terser-webpack-plugin@5.3.16: resolution: { - integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==, + integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==, } engines: { node: ">= 10.13.0" } peerDependencies: @@ -8351,10 +8370,10 @@ packages: uglify-js: optional: true - terser@5.44.0: + terser@5.44.1: resolution: { - integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==, + integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==, } engines: { node: ">=10" } hasBin: true @@ -8633,6 +8652,12 @@ packages: integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==, } + undici-types@7.16.0: + resolution: + { + integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, + } + unified@11.0.5: resolution: { @@ -9365,6 +9390,11 @@ snapshots: tslib: 2.8.1 optional: true + "@emnapi/runtime@1.7.1": + dependencies: + tslib: 2.8.1 + optional: true + "@emnapi/wasi-threads@1.0.4": dependencies: tslib: 2.8.1 @@ -9533,90 +9563,101 @@ snapshots: "@humanwhocodes/retry@0.4.3": {} - "@img/sharp-darwin-arm64@0.34.3": + "@img/colour@1.0.0": + optional: true + + "@img/sharp-darwin-arm64@0.34.5": optionalDependencies: - "@img/sharp-libvips-darwin-arm64": 1.2.0 + "@img/sharp-libvips-darwin-arm64": 1.2.4 optional: true - "@img/sharp-darwin-x64@0.34.3": + "@img/sharp-darwin-x64@0.34.5": optionalDependencies: - "@img/sharp-libvips-darwin-x64": 1.2.0 + "@img/sharp-libvips-darwin-x64": 1.2.4 optional: true - "@img/sharp-libvips-darwin-arm64@1.2.0": + "@img/sharp-libvips-darwin-arm64@1.2.4": optional: true - "@img/sharp-libvips-darwin-x64@1.2.0": + "@img/sharp-libvips-darwin-x64@1.2.4": optional: true - "@img/sharp-libvips-linux-arm64@1.2.0": + "@img/sharp-libvips-linux-arm64@1.2.4": optional: true - "@img/sharp-libvips-linux-arm@1.2.0": + "@img/sharp-libvips-linux-arm@1.2.4": optional: true - "@img/sharp-libvips-linux-ppc64@1.2.0": + "@img/sharp-libvips-linux-ppc64@1.2.4": optional: true - "@img/sharp-libvips-linux-s390x@1.2.0": + "@img/sharp-libvips-linux-riscv64@1.2.4": optional: true - "@img/sharp-libvips-linux-x64@1.2.0": + "@img/sharp-libvips-linux-s390x@1.2.4": optional: true - "@img/sharp-libvips-linuxmusl-arm64@1.2.0": + "@img/sharp-libvips-linux-x64@1.2.4": optional: true - "@img/sharp-libvips-linuxmusl-x64@1.2.0": + "@img/sharp-libvips-linuxmusl-arm64@1.2.4": optional: true - "@img/sharp-linux-arm64@0.34.3": + "@img/sharp-libvips-linuxmusl-x64@1.2.4": + optional: true + + "@img/sharp-linux-arm64@0.34.5": optionalDependencies: - "@img/sharp-libvips-linux-arm64": 1.2.0 + "@img/sharp-libvips-linux-arm64": 1.2.4 optional: true - "@img/sharp-linux-arm@0.34.3": + "@img/sharp-linux-arm@0.34.5": optionalDependencies: - "@img/sharp-libvips-linux-arm": 1.2.0 + "@img/sharp-libvips-linux-arm": 1.2.4 optional: true - "@img/sharp-linux-ppc64@0.34.3": + "@img/sharp-linux-ppc64@0.34.5": optionalDependencies: - "@img/sharp-libvips-linux-ppc64": 1.2.0 + "@img/sharp-libvips-linux-ppc64": 1.2.4 optional: true - "@img/sharp-linux-s390x@0.34.3": + "@img/sharp-linux-riscv64@0.34.5": optionalDependencies: - "@img/sharp-libvips-linux-s390x": 1.2.0 + "@img/sharp-libvips-linux-riscv64": 1.2.4 optional: true - "@img/sharp-linux-x64@0.34.3": + "@img/sharp-linux-s390x@0.34.5": optionalDependencies: - "@img/sharp-libvips-linux-x64": 1.2.0 + "@img/sharp-libvips-linux-s390x": 1.2.4 optional: true - "@img/sharp-linuxmusl-arm64@0.34.3": + "@img/sharp-linux-x64@0.34.5": optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64": 1.2.0 + "@img/sharp-libvips-linux-x64": 1.2.4 optional: true - "@img/sharp-linuxmusl-x64@0.34.3": + "@img/sharp-linuxmusl-arm64@0.34.5": optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64": 1.2.0 + "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 optional: true - "@img/sharp-wasm32@0.34.3": + "@img/sharp-linuxmusl-x64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.2.4 + optional: true + + "@img/sharp-wasm32@0.34.5": dependencies: - "@emnapi/runtime": 1.4.5 + "@emnapi/runtime": 1.7.1 optional: true - "@img/sharp-win32-arm64@0.34.3": + "@img/sharp-win32-arm64@0.34.5": optional: true - "@img/sharp-win32-ia32@0.34.3": + "@img/sharp-win32-ia32@0.34.5": optional: true - "@img/sharp-win32-x64@0.34.3": + "@img/sharp-win32-x64@0.34.5": optional: true "@internationalized/date@3.8.2": @@ -9877,7 +9918,7 @@ snapshots: "@tybys/wasm-util": 0.10.0 optional: true - "@next/env@15.5.7": {} + "@next/env@15.5.9": {} "@next/eslint-plugin-next@15.5.3": dependencies: @@ -10684,7 +10725,7 @@ snapshots: "@sentry/core@8.55.0": {} - "@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)(webpack@5.101.3)": + "@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)(webpack@5.101.3)": dependencies: "@opentelemetry/api": 1.9.0 "@opentelemetry/semantic-conventions": 1.37.0 @@ -10698,7 +10739,7 @@ snapshots: "@sentry/vercel-edge": 10.11.0 "@sentry/webpack-plugin": 4.3.0(webpack@5.101.3) chalk: 3.0.0 - next: 15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + next: 15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) resolve: 1.22.8 rollup: 4.50.1 stacktrace-parser: 0.1.11 @@ -10829,7 +10870,7 @@ snapshots: "@tanstack/query-core": 5.85.9 react: 18.3.1 - "@tsconfig/node10@1.0.11": + "@tsconfig/node10@1.0.12": optional: true "@tsconfig/node12@1.0.11": @@ -10941,9 +10982,9 @@ snapshots: dependencies: undici-types: 7.10.0 - "@types/node@24.3.1": + "@types/node@25.0.2": dependencies: - undici-types: 7.10.0 + undici-types: 7.16.0 "@types/parse-json@4.0.2": {} @@ -12127,6 +12168,8 @@ snapshots: caniuse-lite@1.0.30001734: {} + caniuse-lite@1.0.30001760: {} + ccount@2.0.1: {} chalk@3.0.0: @@ -12205,18 +12248,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - colorette@1.4.0: {} combined-stream@1.0.8: @@ -12335,7 +12366,7 @@ snapshots: detect-libc@1.0.3: optional: true - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true detect-newline@3.1.0: {} @@ -12405,10 +12436,10 @@ snapshots: engine.io-parser@5.2.3: {} - enhanced-resolve@5.18.3: + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.3 + tapable: 2.3.0 err-code@3.0.1: {} @@ -13147,9 +13178,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -13629,7 +13657,7 @@ snapshots: jest-worker@27.5.1: dependencies: - "@types/node": 24.3.1 + "@types/node": 25.0.2 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13738,7 +13766,7 @@ snapshots: lines-and-columns@1.2.4: {} - loader-runner@4.3.0: {} + loader-runner@4.3.1: {} locate-path@5.0.0: dependencies: @@ -14093,13 +14121,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.11(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.11(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: "@babel/runtime": 7.28.2 "@panva/hkdf": 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + next: 15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.27.0 @@ -14113,11 +14141,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0): + next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0): dependencies: - "@next/env": 15.5.7 + "@next/env": 15.5.9 "@swc/helpers": 0.5.15 - caniuse-lite: 1.0.30001734 + caniuse-lite: 1.0.30001760 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14133,7 +14161,7 @@ snapshots: "@next/swc-win32-x64-msvc": 15.5.7 "@opentelemetry/api": 1.9.0 sass: 1.90.0 - sharp: 0.34.3 + sharp: 0.34.5 transitivePeerDependencies: - "@babel/core" - babel-plugin-macros @@ -14159,12 +14187,12 @@ snapshots: dependencies: path-key: 3.1.1 - nuqs@2.4.3(next@15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1): + nuqs@2.4.3(next@15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1): dependencies: mitt: 3.0.1 react: 18.3.1 optionalDependencies: - next: 15.5.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + next: 15.5.9(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) oauth@0.9.15: {} @@ -14734,7 +14762,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - schema-utils@4.3.2: + schema-utils@4.3.3: dependencies: "@types/json-schema": 7.0.15 ajv: 8.17.1 @@ -14749,6 +14777,9 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: + optional: true + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -14775,34 +14806,36 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + "@img/colour": 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - "@img/sharp-darwin-arm64": 0.34.3 - "@img/sharp-darwin-x64": 0.34.3 - "@img/sharp-libvips-darwin-arm64": 1.2.0 - "@img/sharp-libvips-darwin-x64": 1.2.0 - "@img/sharp-libvips-linux-arm": 1.2.0 - "@img/sharp-libvips-linux-arm64": 1.2.0 - "@img/sharp-libvips-linux-ppc64": 1.2.0 - "@img/sharp-libvips-linux-s390x": 1.2.0 - "@img/sharp-libvips-linux-x64": 1.2.0 - "@img/sharp-libvips-linuxmusl-arm64": 1.2.0 - "@img/sharp-libvips-linuxmusl-x64": 1.2.0 - "@img/sharp-linux-arm": 0.34.3 - "@img/sharp-linux-arm64": 0.34.3 - "@img/sharp-linux-ppc64": 0.34.3 - "@img/sharp-linux-s390x": 0.34.3 - "@img/sharp-linux-x64": 0.34.3 - "@img/sharp-linuxmusl-arm64": 0.34.3 - "@img/sharp-linuxmusl-x64": 0.34.3 - "@img/sharp-wasm32": 0.34.3 - "@img/sharp-win32-arm64": 0.34.3 - "@img/sharp-win32-ia32": 0.34.3 - "@img/sharp-win32-x64": 0.34.3 + "@img/sharp-darwin-arm64": 0.34.5 + "@img/sharp-darwin-x64": 0.34.5 + "@img/sharp-libvips-darwin-arm64": 1.2.4 + "@img/sharp-libvips-darwin-x64": 1.2.4 + "@img/sharp-libvips-linux-arm": 1.2.4 + "@img/sharp-libvips-linux-arm64": 1.2.4 + "@img/sharp-libvips-linux-ppc64": 1.2.4 + "@img/sharp-libvips-linux-riscv64": 1.2.4 + "@img/sharp-libvips-linux-s390x": 1.2.4 + "@img/sharp-libvips-linux-x64": 1.2.4 + "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 + "@img/sharp-libvips-linuxmusl-x64": 1.2.4 + "@img/sharp-linux-arm": 0.34.5 + "@img/sharp-linux-arm64": 0.34.5 + "@img/sharp-linux-ppc64": 0.34.5 + "@img/sharp-linux-riscv64": 0.34.5 + "@img/sharp-linux-s390x": 0.34.5 + "@img/sharp-linux-x64": 0.34.5 + "@img/sharp-linuxmusl-arm64": 0.34.5 + "@img/sharp-linuxmusl-x64": 0.34.5 + "@img/sharp-wasm32": 0.34.5 + "@img/sharp-win32-arm64": 0.34.5 + "@img/sharp-win32-ia32": 0.34.5 + "@img/sharp-win32-x64": 0.34.5 optional: true shebang-command@2.0.0: @@ -14857,11 +14890,6 @@ snapshots: transitivePeerDependencies: - supports-color - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - slash@3.0.0: {} socket.io-client@4.7.2: @@ -15086,18 +15114,18 @@ snapshots: transitivePeerDependencies: - ts-node - tapable@2.2.3: {} + tapable@2.3.0: {} - terser-webpack-plugin@5.3.14(webpack@5.101.3): + terser-webpack-plugin@5.3.16(webpack@5.101.3): dependencies: "@jridgewell/trace-mapping": 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.0 + terser: 5.44.1 webpack: 5.101.3 - terser@5.44.0: + terser@5.44.1: dependencies: "@jridgewell/source-map": 0.3.11 acorn: 8.15.0 @@ -15164,7 +15192,7 @@ snapshots: ts-node@10.9.1(@types/node@24.2.1)(typescript@5.9.2): dependencies: "@cspotcode/source-map-support": 0.8.1 - "@tsconfig/node10": 1.0.11 + "@tsconfig/node10": 1.0.12 "@tsconfig/node12": 1.0.11 "@tsconfig/node14": 1.0.3 "@tsconfig/node16": 1.0.4 @@ -15274,6 +15302,8 @@ snapshots: undici-types@7.10.0: {} + undici-types@7.16.0: {} + unified@11.0.5: dependencies: "@types/unist": 3.0.3 @@ -15427,19 +15457,19 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.15.0) browserslist: 4.25.2 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 + enhanced-resolve: 5.18.4 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.3 - terser-webpack-plugin: 5.3.14(webpack@5.101.3) + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(webpack@5.101.3) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: From 443982617d2c7c6ba955fcb721b0b47cd40e4c4b Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Mon, 15 Dec 2025 14:54:05 -0500 Subject: [PATCH 06/12] coolify pull policy (#792) Co-authored-by: Igor Loskutov --- docker-compose.prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 72f404c8..cb067ee5 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,6 +4,7 @@ services: web: image: monadicalsas/reflector-frontend:latest + pull_policy: always environment: - KV_URL=${KV_URL:-redis://redis:6379} - SITE_URL=${SITE_URL} From 16284e1ac3faede2b74f0d91b50c0b5612af2c35 Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Mon, 15 Dec 2025 15:00:53 -0500 Subject: [PATCH 07/12] fix: daily video optimisation (#789) Co-authored-by: Igor Loskutov --- www/app/[roomName]/components/DailyRoom.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/www/app/[roomName]/components/DailyRoom.tsx b/www/app/[roomName]/components/DailyRoom.tsx index 45f2dad2..db051cc6 100644 --- a/www/app/[roomName]/components/DailyRoom.tsx +++ b/www/app/[roomName]/components/DailyRoom.tsx @@ -105,7 +105,19 @@ export default function DailyRoom({ meeting }: DailyRoomProps) { } }); - await frame.join({ url: roomUrl }); + await frame.join({ + url: roomUrl, + sendSettings: { + video: { + // Optimize bandwidth for camera video + // allowAdaptiveLayers automatically adjusts quality based on network conditions + allowAdaptiveLayers: true, + // Use bandwidth-optimized preset as fallback for browsers without adaptive support + maxQuality: "medium", + }, + // Note: screenVideo intentionally not configured to preserve full quality for screen shares + }, + }); } catch (error) { console.error("Error creating Daily frame:", error); } From c62e3c07535694185dbf4bfa84ae306126682fe1 Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Wed, 17 Dec 2025 09:51:55 -0500 Subject: [PATCH 08/12] incorporate daily api undocumented feature (#796) Co-authored-by: Igor Loskutov --- server/reflector/dailyco_api/__init__.py | 2 ++ server/reflector/dailyco_api/responses.py | 28 ++++++++++++++++++-- server/reflector/worker/process.py | 32 +++++++++++++++++++---- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/server/reflector/dailyco_api/__init__.py b/server/reflector/dailyco_api/__init__.py index 8ef95274..65be426e 100644 --- a/server/reflector/dailyco_api/__init__.py +++ b/server/reflector/dailyco_api/__init__.py @@ -18,6 +18,7 @@ from .requests import ( # Response models from .responses import ( + FinishedRecordingResponse, MeetingParticipant, MeetingParticipantsResponse, MeetingResponse, @@ -79,6 +80,7 @@ __all__ = [ "MeetingParticipant", "MeetingResponse", "RecordingResponse", + "FinishedRecordingResponse", "RecordingS3Info", "MeetingTokenResponse", "WebhookResponse", diff --git a/server/reflector/dailyco_api/responses.py b/server/reflector/dailyco_api/responses.py index 279682ae..6ac95188 100644 --- a/server/reflector/dailyco_api/responses.py +++ b/server/reflector/dailyco_api/responses.py @@ -121,7 +121,10 @@ class RecordingS3Info(BaseModel): class RecordingResponse(BaseModel): """ - Response from recording retrieval endpoint. + Response from recording retrieval endpoint (network layer). + + Duration may be None for recordings still being processed by Daily. + Use FinishedRecordingResponse for recordings ready for processing. Reference: https://docs.daily.co/reference/rest-api/recordings """ @@ -135,7 +138,9 @@ class RecordingResponse(BaseModel): max_participants: int | None = Field( None, description="Maximum participants during recording (may be missing)" ) - duration: int = Field(description="Recording duration in seconds") + duration: int | None = Field( + None, description="Recording duration in seconds (None if still processing)" + ) share_token: NonEmptyString | None = Field( None, description="Token for sharing recording" ) @@ -149,6 +154,25 @@ class RecordingResponse(BaseModel): None, description="Meeting session identifier (may be missing)" ) + def to_finished(self) -> "FinishedRecordingResponse | None": + """Convert to FinishedRecordingResponse if duration is available and status is finished.""" + if self.duration is None or self.status != "finished": + return None + return FinishedRecordingResponse(**self.model_dump()) + + +class FinishedRecordingResponse(RecordingResponse): + """ + Recording with confirmed duration - ready for processing. + + This model guarantees duration is present and status is finished. + """ + + status: Literal["finished"] = Field( + description="Recording status (always 'finished')" + ) + duration: int = Field(description="Recording duration in seconds") + class MeetingTokenResponse(BaseModel): """ diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index 21e73723..142d8dc1 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -12,7 +12,7 @@ from celery import shared_task from celery.utils.log import get_task_logger from pydantic import ValidationError -from reflector.dailyco_api import RecordingResponse +from reflector.dailyco_api import FinishedRecordingResponse, RecordingResponse from reflector.db.daily_participant_sessions import ( DailyParticipantSession, daily_participant_sessions_controller, @@ -322,16 +322,38 @@ async def poll_daily_recordings(): ) return - recording_ids = [rec.id for rec in api_recordings] + finished_recordings: List[FinishedRecordingResponse] = [] + for rec in api_recordings: + finished = rec.to_finished() + if finished is None: + logger.debug( + "Skipping unfinished recording", + recording_id=rec.id, + room_name=rec.room_name, + status=rec.status, + ) + continue + finished_recordings.append(finished) + + if not finished_recordings: + logger.debug( + "No finished recordings found from Daily.co API", + total_api_count=len(api_recordings), + ) + return + + recording_ids = [rec.id for rec in finished_recordings] existing_recordings = await recordings_controller.get_by_ids(recording_ids) existing_ids = {rec.id for rec in existing_recordings} - missing_recordings = [rec for rec in api_recordings if rec.id not in existing_ids] + missing_recordings = [ + rec for rec in finished_recordings if rec.id not in existing_ids + ] if not missing_recordings: logger.debug( "All recordings already in DB", - api_count=len(api_recordings), + api_count=len(finished_recordings), existing_count=len(existing_recordings), ) return @@ -339,7 +361,7 @@ async def poll_daily_recordings(): logger.info( "Found recordings missing from DB", missing_count=len(missing_recordings), - total_api_count=len(api_recordings), + total_api_count=len(finished_recordings), existing_count=len(existing_recordings), ) From 0bc971ba966a52d719c8c240b47dc7b3bdea4391 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 18 Dec 2025 20:48:39 +0100 Subject: [PATCH 09/12] fix: main menu login (#800) --- www/app/(auth)/userInfo.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/app/(auth)/userInfo.tsx b/www/app/(auth)/userInfo.tsx index bf6a5b62..f9725a13 100644 --- a/www/app/(auth)/userInfo.tsx +++ b/www/app/(auth)/userInfo.tsx @@ -13,9 +13,12 @@ export default function UserInfo() { ) : !isAuthenticated && !isRefreshing ? ( auth.signIn("authentik")} + onClick={(e) => { + e.preventDefault(); + auth.signIn("authentik"); + }} > Log in From 5f7dfadabd3e8017406ad3720ba495a59963ee34 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 18 Dec 2025 20:49:06 +0100 Subject: [PATCH 10/12] fix: retry on workflow timeout (#798) --- server/reflector/llm.py | 46 +++++++----- server/tests/test_llm_retry.py | 133 ++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 18 deletions(-) diff --git a/server/reflector/llm.py b/server/reflector/llm.py index 0485e847..10ba9138 100644 --- a/server/reflector/llm.py +++ b/server/reflector/llm.py @@ -16,6 +16,9 @@ from llama_index.core.workflow import ( ) from llama_index.llms.openai_like import OpenAILike from pydantic import BaseModel, ValidationError +from workflows.errors import WorkflowTimeoutError + +from reflector.utils.retry import retry T = TypeVar("T", bound=BaseModel) OutputT = TypeVar("OutputT", bound=BaseModel) @@ -231,24 +234,33 @@ class LLM: tone_name: str | None = None, ) -> T: """Get structured output from LLM with validation retry via Workflow.""" - workflow = StructuredOutputWorkflow( - output_cls=output_cls, - max_retries=self.settings_obj.LLM_PARSE_MAX_RETRIES + 1, - timeout=120, - ) - result = await workflow.run( - prompt=prompt, - texts=texts, - tone_name=tone_name, - ) - - if "error" in result: - error_msg = result["error"] or "Max retries exceeded" - raise LLMParseError( + async def run_workflow(): + workflow = StructuredOutputWorkflow( output_cls=output_cls, - error_msg=error_msg, - attempts=result.get("attempts", 0), + max_retries=self.settings_obj.LLM_PARSE_MAX_RETRIES + 1, + timeout=120, ) - return result["success"] + result = await workflow.run( + prompt=prompt, + texts=texts, + tone_name=tone_name, + ) + + if "error" in result: + error_msg = result["error"] or "Max retries exceeded" + raise LLMParseError( + output_cls=output_cls, + error_msg=error_msg, + attempts=result.get("attempts", 0), + ) + + return result["success"] + + return await retry(run_workflow)( + retry_attempts=3, + retry_backoff_interval=1.0, + retry_backoff_max=30.0, + retry_ignore_exc_types=(WorkflowTimeoutError,), + ) diff --git a/server/tests/test_llm_retry.py b/server/tests/test_llm_retry.py index f9fe28b4..5a43c8c5 100644 --- a/server/tests/test_llm_retry.py +++ b/server/tests/test_llm_retry.py @@ -1,12 +1,14 @@ """Tests for LLM parse error recovery using llama-index Workflow""" +from time import monotonic from unittest.mock import AsyncMock, MagicMock, patch import pytest from pydantic import BaseModel, Field -from workflows.errors import WorkflowRuntimeError +from workflows.errors import WorkflowRuntimeError, WorkflowTimeoutError from reflector.llm import LLM, LLMParseError, StructuredOutputWorkflow +from reflector.utils.retry import RetryException class TestResponse(BaseModel): @@ -355,3 +357,132 @@ class TestNetworkErrorRetries: # Only called once - Workflow doesn't retry network errors assert mock_settings.llm.acomplete.call_count == 1 + + +class TestWorkflowTimeoutRetry: + """Test timeout retry mechanism in get_structured_response""" + + @pytest.mark.asyncio + async def test_timeout_retry_succeeds_on_retry(self, test_settings): + """Test that WorkflowTimeoutError triggers retry and succeeds""" + llm = LLM(settings=test_settings, temperature=0.4, max_tokens=100) + + call_count = {"count": 0} + + async def workflow_run_side_effect(*args, **kwargs): + call_count["count"] += 1 + if call_count["count"] == 1: + raise WorkflowTimeoutError("Operation timed out after 120 seconds") + return { + "success": TestResponse( + title="Test", summary="Summary", confidence=0.95 + ) + } + + with ( + patch("reflector.llm.StructuredOutputWorkflow") as mock_workflow_class, + patch("reflector.llm.TreeSummarize") as mock_summarize, + patch("reflector.llm.Settings") as mock_settings, + ): + mock_workflow = MagicMock() + mock_workflow.run = AsyncMock(side_effect=workflow_run_side_effect) + mock_workflow_class.return_value = mock_workflow + + mock_summarizer = MagicMock() + mock_summarize.return_value = mock_summarizer + mock_summarizer.aget_response = AsyncMock(return_value="Some analysis") + mock_settings.llm.acomplete = AsyncMock( + return_value=make_completion_response( + '{"title": "Test", "summary": "Summary", "confidence": 0.95}' + ) + ) + + result = await llm.get_structured_response( + prompt="Test prompt", texts=["Test text"], output_cls=TestResponse + ) + + assert result.title == "Test" + assert result.summary == "Summary" + assert call_count["count"] == 2 + + @pytest.mark.asyncio + async def test_timeout_retry_exhausts_after_max_attempts(self, test_settings): + """Test that timeout retry stops after max attempts""" + llm = LLM(settings=test_settings, temperature=0.4, max_tokens=100) + + call_count = {"count": 0} + + async def workflow_run_side_effect(*args, **kwargs): + call_count["count"] += 1 + raise WorkflowTimeoutError("Operation timed out after 120 seconds") + + with ( + patch("reflector.llm.StructuredOutputWorkflow") as mock_workflow_class, + patch("reflector.llm.TreeSummarize") as mock_summarize, + patch("reflector.llm.Settings") as mock_settings, + ): + mock_workflow = MagicMock() + mock_workflow.run = AsyncMock(side_effect=workflow_run_side_effect) + mock_workflow_class.return_value = mock_workflow + + mock_summarizer = MagicMock() + mock_summarize.return_value = mock_summarizer + mock_summarizer.aget_response = AsyncMock(return_value="Some analysis") + mock_settings.llm.acomplete = AsyncMock( + return_value=make_completion_response( + '{"title": "Test", "summary": "Summary", "confidence": 0.95}' + ) + ) + + with pytest.raises(RetryException, match="Retry attempts exceeded"): + await llm.get_structured_response( + prompt="Test prompt", texts=["Test text"], output_cls=TestResponse + ) + + assert call_count["count"] == 3 + + @pytest.mark.asyncio + async def test_timeout_retry_with_backoff(self, test_settings): + """Test that exponential backoff is applied between retries""" + llm = LLM(settings=test_settings, temperature=0.4, max_tokens=100) + + call_times = [] + + async def workflow_run_side_effect(*args, **kwargs): + call_times.append(monotonic()) + if len(call_times) < 3: + raise WorkflowTimeoutError("Operation timed out after 120 seconds") + return { + "success": TestResponse( + title="Test", summary="Summary", confidence=0.95 + ) + } + + with ( + patch("reflector.llm.StructuredOutputWorkflow") as mock_workflow_class, + patch("reflector.llm.TreeSummarize") as mock_summarize, + patch("reflector.llm.Settings") as mock_settings, + ): + mock_workflow = MagicMock() + mock_workflow.run = AsyncMock(side_effect=workflow_run_side_effect) + mock_workflow_class.return_value = mock_workflow + + mock_summarizer = MagicMock() + mock_summarize.return_value = mock_summarizer + mock_summarizer.aget_response = AsyncMock(return_value="Some analysis") + mock_settings.llm.acomplete = AsyncMock( + return_value=make_completion_response( + '{"title": "Test", "summary": "Summary", "confidence": 0.95}' + ) + ) + + result = await llm.get_structured_response( + prompt="Test prompt", texts=["Test text"], output_cls=TestResponse + ) + + assert result.title == "Test" + if len(call_times) >= 2: + time_between_calls = call_times[1] - call_times[0] + assert ( + time_between_calls >= 1.5 + ), f"Expected ~2s backoff, got {time_between_calls}s" From 5f458aa4a7ec3d00ca5ec49d62fcc8ad232b138e Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 18 Dec 2025 21:10:04 +0100 Subject: [PATCH 11/12] fix: automatically reprocess daily recordings (#797) * Automatically reprocess recordings * Restore the comments * Remove redundant check * Fix indent * Add comment about cyclic import --- server/reflector/db/recordings.py | 31 ++++++++++ server/reflector/worker/app.py | 4 ++ server/reflector/worker/process.py | 99 +++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/server/reflector/db/recordings.py b/server/reflector/db/recordings.py index 18fe358b..82609b38 100644 --- a/server/reflector/db/recordings.py +++ b/server/reflector/db/recordings.py @@ -3,6 +3,7 @@ from typing import Literal import sqlalchemy as sa from pydantic import BaseModel, Field +from sqlalchemy import or_ from reflector.db import get_database, metadata from reflector.utils import generate_uuid4 @@ -79,5 +80,35 @@ class RecordingController: results = await get_database().fetch_all(query) return [Recording(**row) for row in results] + async def get_multitrack_needing_reprocessing( + self, bucket_name: str + ) -> list[Recording]: + """ + Get multitrack recordings that need reprocessing: + - Have track_keys (multitrack) + - Either have no transcript OR transcript has error status + + This is more efficient than fetching all recordings and filtering in Python. + """ + from reflector.db.transcripts import ( + transcripts, # noqa: PLC0415 cyclic import + ) + + query = ( + recordings.select() + .outerjoin(transcripts, recordings.c.id == transcripts.c.recording_id) + .where( + recordings.c.bucket_name == bucket_name, + recordings.c.track_keys.isnot(None), + or_( + transcripts.c.id.is_(None), + transcripts.c.status == "error", + ), + ) + ) + results = await get_database().fetch_all(query) + recordings_list = [Recording(**row) for row in results] + return [r for r in recordings_list if r.is_multitrack] + recordings_controller = RecordingController() diff --git a/server/reflector/worker/app.py b/server/reflector/worker/app.py index c0e711ae..b1256c94 100644 --- a/server/reflector/worker/app.py +++ b/server/reflector/worker/app.py @@ -38,6 +38,10 @@ else: "task": "reflector.worker.process.reprocess_failed_recordings", "schedule": crontab(hour=5, minute=0), # Midnight EST }, + "reprocess_failed_daily_recordings": { + "task": "reflector.worker.process.reprocess_failed_daily_recordings", + "schedule": crontab(hour=5, minute=0), # Midnight EST + }, "poll_daily_recordings": { "task": "reflector.worker.process.poll_daily_recordings", "schedule": 180.0, # Every 3 minutes (configurable lookback window) diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index 142d8dc1..ab163fad 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -671,7 +671,7 @@ async def reprocess_failed_recordings(): Find recordings in Whereby S3 bucket and check if they have proper transcriptions. If not, requeue them for processing. - Note: Daily.co recordings are processed via webhooks, not this cron job. + Note: Daily.co multitrack recordings are handled by reprocess_failed_daily_recordings. """ logger.info("Checking Whereby recordings that need processing or reprocessing") @@ -724,6 +724,103 @@ async def reprocess_failed_recordings(): return reprocessed_count +@shared_task +@asynctask +async def reprocess_failed_daily_recordings(): + """ + Find Daily.co multitrack recordings in the database and check if they have proper transcriptions. + If not, requeue them for processing. + """ + logger.info( + "Checking Daily.co multitrack recordings that need processing or reprocessing" + ) + + if not settings.DAILYCO_STORAGE_AWS_BUCKET_NAME: + logger.debug( + "DAILYCO_STORAGE_AWS_BUCKET_NAME not configured; skipping Daily recording reprocessing" + ) + return 0 + + bucket_name = settings.DAILYCO_STORAGE_AWS_BUCKET_NAME + reprocessed_count = 0 + + try: + multitrack_recordings = ( + await recordings_controller.get_multitrack_needing_reprocessing(bucket_name) + ) + + logger.info( + "Found multitrack recordings needing reprocessing", + count=len(multitrack_recordings), + bucket=bucket_name, + ) + + for recording in multitrack_recordings: + if not recording.meeting_id: + logger.debug( + "Skipping recording without meeting_id", + recording_id=recording.id, + ) + continue + + meeting = await meetings_controller.get_by_id(recording.meeting_id) + if not meeting: + logger.warning( + "Meeting not found for recording", + recording_id=recording.id, + meeting_id=recording.meeting_id, + ) + continue + + transcript = None + try: + transcript = await transcripts_controller.get_by_recording_id( + recording.id + ) + except ValidationError: + await transcripts_controller.remove_by_recording_id(recording.id) + logger.warning( + "Removed invalid transcript for recording", + recording_id=recording.id, + ) + + if not recording.track_keys: + logger.warning( + "Recording has no track_keys, cannot reprocess", + recording_id=recording.id, + ) + continue + + logger.info( + "Queueing Daily recording for reprocessing", + recording_id=recording.id, + room_name=meeting.room_name, + track_count=len(recording.track_keys), + transcript_status=transcript.status if transcript else None, + ) + + process_multitrack_recording.delay( + bucket_name=bucket_name, + daily_room_name=meeting.room_name, + recording_id=recording.id, + track_keys=recording.track_keys, + ) + reprocessed_count += 1 + + except Exception as e: + logger.error( + "Error checking Daily multitrack recordings", + error=str(e), + exc_info=True, + ) + + logger.info( + "Daily reprocessing complete", + requeued_count=reprocessed_count, + ) + return reprocessed_count + + @shared_task @asynctask async def trigger_daily_reconciliation() -> None: From 964cd78bb699d83d012ae4b8c96565df25b90a5d Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Thu, 18 Dec 2025 21:13:47 +0100 Subject: [PATCH 12/12] feat: identify action items (#790) * Identify action items * Add action items to mock summary * Add action items validator * Remove final prefix from action items * Make on action items callback required * Don't mutation action items response * Assign action items to none on error * Use timeout constant * Exclude action items from transcript list --- .../versions/05f8688d6895_add_action_items.py | 26 +++ server/reflector/db/transcripts.py | 13 +- server/reflector/llm.py | 5 +- .../reflector/pipelines/main_file_pipeline.py | 1 + .../reflector/pipelines/main_live_pipeline.py | 19 ++ .../pipelines/main_multitrack_pipeline.py | 1 + .../reflector/pipelines/topic_processing.py | 14 +- .../processors/summary/summary_builder.py | 195 +++++++++++++++++- .../processors/transcript_final_summary.py | 26 ++- .../processors/transcript_topic_detector.py | 6 +- server/reflector/processors/types.py | 4 + server/reflector/settings.py | 3 + server/reflector/views/transcripts.py | 1 + server/reflector/worker/webhook.py | 1 + server/tests/test_pipeline_main_file.py | 15 +- 15 files changed, 306 insertions(+), 24 deletions(-) create mode 100644 server/migrations/versions/05f8688d6895_add_action_items.py diff --git a/server/migrations/versions/05f8688d6895_add_action_items.py b/server/migrations/versions/05f8688d6895_add_action_items.py new file mode 100644 index 00000000..b789263f --- /dev/null +++ b/server/migrations/versions/05f8688d6895_add_action_items.py @@ -0,0 +1,26 @@ +"""add_action_items + +Revision ID: 05f8688d6895 +Revises: bbafedfa510c +Create Date: 2025-12-12 11:57:50.209658 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "05f8688d6895" +down_revision: Union[str, None] = "bbafedfa510c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("transcript", sa.Column("action_items", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("transcript", "action_items") diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index f9c3c057..736075c8 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -44,6 +44,7 @@ transcripts = sqlalchemy.Table( sqlalchemy.Column("title", sqlalchemy.String), sqlalchemy.Column("short_summary", sqlalchemy.String), sqlalchemy.Column("long_summary", sqlalchemy.String), + sqlalchemy.Column("action_items", sqlalchemy.JSON), sqlalchemy.Column("topics", sqlalchemy.JSON), sqlalchemy.Column("events", sqlalchemy.JSON), sqlalchemy.Column("participants", sqlalchemy.JSON), @@ -164,6 +165,10 @@ class TranscriptFinalLongSummary(BaseModel): long_summary: str +class TranscriptActionItems(BaseModel): + action_items: dict + + class TranscriptFinalTitle(BaseModel): title: str @@ -204,6 +209,7 @@ class Transcript(BaseModel): locked: bool = False short_summary: str | None = None long_summary: str | None = None + action_items: dict | None = None topics: list[TranscriptTopic] = [] events: list[TranscriptEvent] = [] participants: list[TranscriptParticipant] | None = [] @@ -368,7 +374,12 @@ class TranscriptController: room_id: str | None = None, search_term: str | None = None, return_query: bool = False, - exclude_columns: list[str] = ["topics", "events", "participants"], + exclude_columns: list[str] = [ + "topics", + "events", + "participants", + "action_items", + ], ) -> list[Transcript]: """ Get all transcripts diff --git a/server/reflector/llm.py b/server/reflector/llm.py index 10ba9138..f7c9137d 100644 --- a/server/reflector/llm.py +++ b/server/reflector/llm.py @@ -232,14 +232,17 @@ class LLM: texts: list[str], output_cls: Type[T], tone_name: str | None = None, + timeout: int | None = None, ) -> T: """Get structured output from LLM with validation retry via Workflow.""" + if timeout is None: + timeout = self.settings_obj.LLM_STRUCTURED_RESPONSE_TIMEOUT async def run_workflow(): workflow = StructuredOutputWorkflow( output_cls=output_cls, max_retries=self.settings_obj.LLM_PARSE_MAX_RETRIES + 1, - timeout=120, + timeout=timeout, ) result = await workflow.run( diff --git a/server/reflector/pipelines/main_file_pipeline.py b/server/reflector/pipelines/main_file_pipeline.py index aff6e042..b058e5b9 100644 --- a/server/reflector/pipelines/main_file_pipeline.py +++ b/server/reflector/pipelines/main_file_pipeline.py @@ -309,6 +309,7 @@ class PipelineMainFile(PipelineMainBase): transcript, on_long_summary_callback=self.on_long_summary, on_short_summary_callback=self.on_short_summary, + on_action_items_callback=self.on_action_items, empty_pipeline=self.empty_pipeline, logger=self.logger, ) diff --git a/server/reflector/pipelines/main_live_pipeline.py b/server/reflector/pipelines/main_live_pipeline.py index 83e560d6..2b6c6d07 100644 --- a/server/reflector/pipelines/main_live_pipeline.py +++ b/server/reflector/pipelines/main_live_pipeline.py @@ -27,6 +27,7 @@ from reflector.db.recordings import recordings_controller from reflector.db.rooms import rooms_controller from reflector.db.transcripts import ( Transcript, + TranscriptActionItems, TranscriptDuration, TranscriptFinalLongSummary, TranscriptFinalShortSummary, @@ -306,6 +307,23 @@ class PipelineMainBase(PipelineRunner[PipelineMessage], Generic[PipelineMessage] data=final_short_summary, ) + @broadcast_to_sockets + async def on_action_items(self, data): + action_items = TranscriptActionItems(action_items=data.action_items) + async with self.transaction(): + transcript = await self.get_transcript() + await transcripts_controller.update( + transcript, + { + "action_items": action_items.action_items, + }, + ) + return await transcripts_controller.append_event( + transcript=transcript, + event="ACTION_ITEMS", + data=action_items, + ) + @broadcast_to_sockets async def on_duration(self, data): async with self.transaction(): @@ -465,6 +483,7 @@ class PipelineMainFinalSummaries(PipelineMainFromTopics): transcript=self._transcript, callback=self.on_long_summary, on_short_summary=self.on_short_summary, + on_action_items=self.on_action_items, ), ] diff --git a/server/reflector/pipelines/main_multitrack_pipeline.py b/server/reflector/pipelines/main_multitrack_pipeline.py index 579bfbd3..d3b28274 100644 --- a/server/reflector/pipelines/main_multitrack_pipeline.py +++ b/server/reflector/pipelines/main_multitrack_pipeline.py @@ -772,6 +772,7 @@ class PipelineMainMultitrack(PipelineMainBase): transcript, on_long_summary_callback=self.on_long_summary, on_short_summary_callback=self.on_short_summary, + on_action_items_callback=self.on_action_items, empty_pipeline=self.empty_pipeline, logger=self.logger, ) diff --git a/server/reflector/pipelines/topic_processing.py b/server/reflector/pipelines/topic_processing.py index 7f055025..a6189d63 100644 --- a/server/reflector/pipelines/topic_processing.py +++ b/server/reflector/pipelines/topic_processing.py @@ -89,6 +89,7 @@ async def generate_summaries( *, on_long_summary_callback: Callable, on_short_summary_callback: Callable, + on_action_items_callback: Callable, empty_pipeline: EmptyPipeline, logger: structlog.BoundLogger, ): @@ -96,11 +97,14 @@ async def generate_summaries( logger.warning("No topics for summary generation") return - processor = TranscriptFinalSummaryProcessor( - transcript=transcript, - callback=on_long_summary_callback, - on_short_summary=on_short_summary_callback, - ) + processor_kwargs = { + "transcript": transcript, + "callback": on_long_summary_callback, + "on_short_summary": on_short_summary_callback, + "on_action_items": on_action_items_callback, + } + + processor = TranscriptFinalSummaryProcessor(**processor_kwargs) processor.set_pipeline(empty_pipeline) for topic in topics: diff --git a/server/reflector/processors/summary/summary_builder.py b/server/reflector/processors/summary/summary_builder.py index df348093..b9770fc5 100644 --- a/server/reflector/processors/summary/summary_builder.py +++ b/server/reflector/processors/summary/summary_builder.py @@ -96,6 +96,36 @@ RECAP_PROMPT = dedent( """ ).strip() +ACTION_ITEMS_PROMPT = dedent( + """ + Identify action items from this meeting transcript. Your goal is to identify what was decided and what needs to happen next. + + Look for: + + 1. **Decisions Made**: Any decisions, choices, or conclusions reached during the meeting. For each decision: + - What was decided? (be specific) + - Who made the decision or was involved? (use actual participant names) + - Why was this decision made? (key factors, reasoning, or rationale) + + 2. **Next Steps / Action Items**: Any tasks, follow-ups, or actions that were mentioned or assigned. For each action item: + - What specific task needs to be done? (be concrete and actionable) + - Who is responsible? (use actual participant names if mentioned, or "team" if unclear) + - When is it due? (any deadlines, timeframes, or "by next meeting" type commitments) + - What context is needed? (any additional details that help understand the task) + + Guidelines: + - Be thorough and identify all action items, even if they seem minor + - Include items that were agreed upon, assigned, or committed to + - Include decisions even if they seem obvious or implicit + - If someone says "I'll do X" or "We should do Y", that's an action item + - If someone says "Let's go with option A", that's a decision + - Use the exact participant names from the transcript + - If no participant name is mentioned, you can leave assigned_to/decided_by as null + + Only return empty lists if the transcript contains NO decisions and NO action items whatsoever. + """ +).strip() + STRUCTURED_RESPONSE_PROMPT_TEMPLATE = dedent( """ Based on the following analysis, provide the information in the requested JSON format: @@ -155,6 +185,53 @@ class SubjectsResponse(BaseModel): ) +class ActionItem(BaseModel): + """A single action item from the meeting""" + + task: str = Field(description="The task or action item to be completed") + assigned_to: str | None = Field( + default=None, description="Person or team assigned to this task (name)" + ) + assigned_to_participant_id: str | None = Field( + default=None, description="Participant ID if assigned_to matches a participant" + ) + deadline: str | None = Field( + default=None, description="Deadline or timeframe mentioned for this task" + ) + context: str | None = Field( + default=None, description="Additional context or notes about this task" + ) + + +class Decision(BaseModel): + """A decision made during the meeting""" + + decision: str = Field(description="What was decided") + rationale: str | None = Field( + default=None, + description="Reasoning or key factors that influenced this decision", + ) + decided_by: str | None = Field( + default=None, description="Person or group who made the decision (name)" + ) + decided_by_participant_id: str | None = Field( + default=None, description="Participant ID if decided_by matches a participant" + ) + + +class ActionItemsResponse(BaseModel): + """Pydantic model for identified action items""" + + decisions: list[Decision] = Field( + default_factory=list, + description="List of decisions made during the meeting", + ) + next_steps: list[ActionItem] = Field( + default_factory=list, + description="List of action items and next steps to be taken", + ) + + class SummaryBuilder: def __init__(self, llm: LLM, filename: str | None = None, logger=None) -> None: self.transcript: str | None = None @@ -166,6 +243,8 @@ class SummaryBuilder: self.model_name: str = llm.model_name self.logger = logger or structlog.get_logger() self.participant_instructions: str | None = None + self.action_items: ActionItemsResponse | None = None + self.participant_name_to_id: dict[str, str] = {} if filename: self.read_transcript_from_file(filename) @@ -189,13 +268,20 @@ class SummaryBuilder: self.llm = llm async def _get_structured_response( - self, prompt: str, output_cls: Type[T], tone_name: str | None = None + self, + prompt: str, + output_cls: Type[T], + tone_name: str | None = None, + timeout: int | None = None, ) -> T: """Generic function to get structured output from LLM for non-function-calling models.""" - # Add participant instructions to the prompt if available enhanced_prompt = self._enhance_prompt_with_participants(prompt) return await self.llm.get_structured_response( - enhanced_prompt, [self.transcript], output_cls, tone_name=tone_name + enhanced_prompt, + [self.transcript], + output_cls, + tone_name=tone_name, + timeout=timeout, ) async def _get_response( @@ -216,11 +302,19 @@ class SummaryBuilder: # Participants # ---------------------------------------------------------------------------- - def set_known_participants(self, participants: list[str]) -> None: + def set_known_participants( + self, + participants: list[str], + participant_name_to_id: dict[str, str] | None = None, + ) -> None: """ Set known participants directly without LLM identification. This is used when participants are already identified and stored. They are appended at the end of the transcript, providing more context for the assistant. + + Args: + participants: List of participant names + participant_name_to_id: Optional mapping of participant names to their IDs """ if not participants: self.logger.warning("No participants provided") @@ -231,10 +325,12 @@ class SummaryBuilder: participants=participants, ) + if participant_name_to_id: + self.participant_name_to_id = participant_name_to_id + participants_md = self.format_list_md(participants) self.transcript += f"\n\n# Participants\n\n{participants_md}" - # Set instructions that will be automatically added to all prompts participants_list = ", ".join(participants) self.participant_instructions = dedent( f""" @@ -413,6 +509,92 @@ class SummaryBuilder: self.recap = str(recap_response) self.logger.info(f"Quick recap: {self.recap}") + def _map_participant_names_to_ids( + self, response: ActionItemsResponse + ) -> ActionItemsResponse: + """Map participant names in action items to participant IDs.""" + if not self.participant_name_to_id: + return response + + decisions = [] + for decision in response.decisions: + new_decision = decision.model_copy() + if ( + decision.decided_by + and decision.decided_by in self.participant_name_to_id + ): + new_decision.decided_by_participant_id = self.participant_name_to_id[ + decision.decided_by + ] + decisions.append(new_decision) + + next_steps = [] + for item in response.next_steps: + new_item = item.model_copy() + if item.assigned_to and item.assigned_to in self.participant_name_to_id: + new_item.assigned_to_participant_id = self.participant_name_to_id[ + item.assigned_to + ] + next_steps.append(new_item) + + return ActionItemsResponse(decisions=decisions, next_steps=next_steps) + + async def identify_action_items(self) -> ActionItemsResponse | None: + """Identify action items (decisions and next steps) from the transcript.""" + self.logger.info("--- identify action items using TreeSummarize") + + if not self.transcript: + self.logger.warning( + "No transcript available for action items identification" + ) + self.action_items = None + return None + + action_items_prompt = ACTION_ITEMS_PROMPT + + try: + response = await self._get_structured_response( + action_items_prompt, + ActionItemsResponse, + tone_name="Action item identifier", + timeout=settings.LLM_STRUCTURED_RESPONSE_TIMEOUT, + ) + + response = self._map_participant_names_to_ids(response) + + self.action_items = response + self.logger.info( + f"Identified {len(response.decisions)} decisions and {len(response.next_steps)} action items", + decisions_count=len(response.decisions), + next_steps_count=len(response.next_steps), + ) + + if response.decisions: + self.logger.debug( + "Decisions identified", + decisions=[d.decision for d in response.decisions], + ) + if response.next_steps: + self.logger.debug( + "Action items identified", + tasks=[item.task for item in response.next_steps], + ) + if not response.decisions and not response.next_steps: + self.logger.warning( + "No action items identified from transcript", + transcript_length=len(self.transcript), + ) + + return response + + except Exception as e: + self.logger.error( + f"Error identifying action items: {e}", + exc_info=True, + ) + self.action_items = None + return None + async def generate_summary(self, only_subjects: bool = False) -> None: """ Generate summary by extracting subjects, creating summaries for each, and generating a recap. @@ -424,6 +606,7 @@ class SummaryBuilder: await self.generate_subject_summaries() await self.generate_recap() + await self.identify_action_items() # ---------------------------------------------------------------------------- # Markdown @@ -526,8 +709,6 @@ if __name__ == "__main__": if args.summary: await sm.generate_summary() - # Note: action items generation has been removed - print("") print("-" * 80) print("") diff --git a/server/reflector/processors/transcript_final_summary.py b/server/reflector/processors/transcript_final_summary.py index dfe07aad..932a46be 100644 --- a/server/reflector/processors/transcript_final_summary.py +++ b/server/reflector/processors/transcript_final_summary.py @@ -1,7 +1,12 @@ from reflector.llm import LLM from reflector.processors.base import Processor from reflector.processors.summary.summary_builder import SummaryBuilder -from reflector.processors.types import FinalLongSummary, FinalShortSummary, TitleSummary +from reflector.processors.types import ( + ActionItems, + FinalLongSummary, + FinalShortSummary, + TitleSummary, +) from reflector.settings import settings @@ -27,15 +32,20 @@ class TranscriptFinalSummaryProcessor(Processor): builder = SummaryBuilder(self.llm, logger=self.logger) builder.set_transcript(text) - # Use known participants if available, otherwise identify them if self.transcript and self.transcript.participants: - # Extract participant names from the stored participants participant_names = [p.name for p in self.transcript.participants if p.name] if participant_names: self.logger.info( f"Using {len(participant_names)} known participants from transcript" ) - builder.set_known_participants(participant_names) + participant_name_to_id = { + p.name: p.id + for p in self.transcript.participants + if p.name and p.id + } + builder.set_known_participants( + participant_names, participant_name_to_id=participant_name_to_id + ) else: self.logger.info( "Participants field exists but is empty, identifying participants" @@ -63,7 +73,6 @@ class TranscriptFinalSummaryProcessor(Processor): self.logger.warning("No summary to output") return - # build the speakermap from the transcript speakermap = {} if self.transcript: speakermap = { @@ -76,8 +85,6 @@ class TranscriptFinalSummaryProcessor(Processor): speakermap=speakermap, ) - # build the transcript as a single string - # Replace speaker IDs with actual participant names if available text_transcript = [] unique_speakers = set() for topic in self.chunks: @@ -111,4 +118,9 @@ class TranscriptFinalSummaryProcessor(Processor): ) await self.emit(final_short_summary, name="short_summary") + if self.builder and self.builder.action_items: + action_items = self.builder.action_items.model_dump() + action_items = ActionItems(action_items=action_items) + await self.emit(action_items, name="action_items") + await self.emit(final_long_summary) diff --git a/server/reflector/processors/transcript_topic_detector.py b/server/reflector/processors/transcript_topic_detector.py index 695d3af3..154db0ec 100644 --- a/server/reflector/processors/transcript_topic_detector.py +++ b/server/reflector/processors/transcript_topic_detector.py @@ -78,7 +78,11 @@ class TranscriptTopicDetectorProcessor(Processor): """ prompt = TOPIC_PROMPT.format(text=text) response = await self.llm.get_structured_response( - prompt, [text], TopicResponse, tone_name="Topic analyzer" + prompt, + [text], + TopicResponse, + tone_name="Topic analyzer", + timeout=settings.LLM_STRUCTURED_RESPONSE_TIMEOUT, ) return response diff --git a/server/reflector/processors/types.py b/server/reflector/processors/types.py index 3369e09c..ca6d675f 100644 --- a/server/reflector/processors/types.py +++ b/server/reflector/processors/types.py @@ -264,6 +264,10 @@ class FinalShortSummary(BaseModel): duration: float +class ActionItems(BaseModel): + action_items: dict # JSON-serializable dict from ActionItemsResponse + + class FinalTitle(BaseModel): title: str diff --git a/server/reflector/settings.py b/server/reflector/settings.py index 12276121..a72981b9 100644 --- a/server/reflector/settings.py +++ b/server/reflector/settings.py @@ -77,6 +77,9 @@ class Settings(BaseSettings): LLM_PARSE_MAX_RETRIES: int = ( 3 # Max retries for JSON/validation errors (total attempts = retries + 1) ) + LLM_STRUCTURED_RESPONSE_TIMEOUT: int = ( + 300 # Timeout in seconds for structured responses (5 minutes) + ) # Diarization DIARIZATION_ENABLED: bool = True diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 02c1aa3e..2e1c9d30 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -501,6 +501,7 @@ async def transcript_get( "title": transcript.title, "short_summary": transcript.short_summary, "long_summary": transcript.long_summary, + "action_items": transcript.action_items, "created_at": transcript.created_at, "share_mode": transcript.share_mode, "source_language": transcript.source_language, diff --git a/server/reflector/worker/webhook.py b/server/reflector/worker/webhook.py index 57b294d8..d58cc7b3 100644 --- a/server/reflector/worker/webhook.py +++ b/server/reflector/worker/webhook.py @@ -123,6 +123,7 @@ async def send_transcript_webhook( "target_language": transcript.target_language, "status": transcript.status, "frontend_url": frontend_url, + "action_items": transcript.action_items, }, "room": { "id": room.id, diff --git a/server/tests/test_pipeline_main_file.py b/server/tests/test_pipeline_main_file.py index 825c8389..8a4d63dd 100644 --- a/server/tests/test_pipeline_main_file.py +++ b/server/tests/test_pipeline_main_file.py @@ -266,7 +266,11 @@ async def mock_summary_processor(): # When flush is called, simulate summary generation by calling the callbacks async def flush_with_callback(): mock_summary.flush_called = True - from reflector.processors.types import FinalLongSummary, FinalShortSummary + from reflector.processors.types import ( + ActionItems, + FinalLongSummary, + FinalShortSummary, + ) if hasattr(mock_summary, "_callback"): await mock_summary._callback( @@ -276,12 +280,19 @@ async def mock_summary_processor(): await mock_summary._on_short_summary( FinalShortSummary(short_summary="Test short summary", duration=10.0) ) + if hasattr(mock_summary, "_on_action_items"): + await mock_summary._on_action_items( + ActionItems(action_items={"test": "action item"}) + ) mock_summary.flush = flush_with_callback - def init_with_callback(transcript=None, callback=None, on_short_summary=None): + def init_with_callback( + transcript=None, callback=None, on_short_summary=None, on_action_items=None + ): mock_summary._callback = callback mock_summary._on_short_summary = on_short_summary + mock_summary._on_action_items = on_action_items return mock_summary mock_summary_class.side_effect = init_with_callback