1515from iris .domain .status .activity_dto import ActivityKind
1616from iris .pipeline .shared .mcq_generation_pipeline import McqGenerationPipeline
1717from iris .retrieval .lecture .lecture_retrieval_utils import should_allow_lecture_tool
18+ from iris .retrieval .lecture .lecture_visibility import (
19+ is_slide_visible ,
20+ is_unit_released ,
21+ )
1822from iris .vector_database .lecture_unit_page_chunk_schema import (
1923 LectureUnitPageChunkSchema ,
2024)
2327logger = get_logger (__name__ )
2428
2529_MAX_MCQ_COUNT = 10
30+ _MCQ_CHUNK_PAGE_SIZE = 100
31+ _MAX_MCQ_CANDIDATES = 10_000
32+ _MAX_MCQ_VISIBLE_CHUNKS = 50
33+ _MAX_MCQ_CONTENT_CHARS = 40_000
2634
2735
2836def detect_mcq_intent (user_message : str ) -> tuple [bool , int ]:
@@ -74,6 +82,7 @@ def detect_mcq_intent(user_message: str) -> tuple[bool, int]:
7482def retrieve_lecture_content_for_mcq (
7583 db : Any ,
7684 course_id : int ,
85+ base_url : str ,
7786 lecture_id : Optional [int ] = None ,
7887 allow_lecture_tool : Optional [bool ] = None ,
7988) -> tuple [Optional [str ], list [dict ]]:
@@ -85,6 +94,7 @@ def retrieve_lecture_content_for_mcq(
8594 Args:
8695 db: The Weaviate database client wrapper.
8796 course_id: ID of the course.
97+ base_url: Artemis instance URL used to isolate colliding local IDs.
8898 lecture_id: Optional lecture ID to narrow results.
8999 allow_lecture_tool: Pre-computed lecture availability flag (e.g. from
90100 ``prepare_state``). Pass it to skip the redundant Weaviate check.
@@ -100,74 +110,118 @@ def retrieve_lecture_content_for_mcq(
100110 chunk_filter = Filter .by_property (
101111 LectureUnitPageChunkSchema .COURSE_ID .value
102112 ).equal (course_id )
113+ chunk_filter &= Filter .by_property (
114+ LectureUnitPageChunkSchema .BASE_URL .value
115+ ).equal (base_url )
103116
104117 if lecture_id is not None :
105118 chunk_filter &= Filter .by_property (
106119 LectureUnitPageChunkSchema .LECTURE_ID .value
107120 ).equal (lecture_id )
108121
109- chunks = db .lectures .query .fetch_objects (
110- filters = chunk_filter ,
111- return_properties = [
112- LectureUnitPageChunkSchema .LECTURE_UNIT_ID .value ,
113- LectureUnitPageChunkSchema .LECTURE_ID .value ,
114- LectureUnitPageChunkSchema .PAGE_NUMBER .value ,
115- LectureUnitPageChunkSchema .PAGE_TEXT_CONTENT .value ,
116- ],
117- )
118-
119- if not chunks .objects :
120- return None , []
121-
122122 unit_filter = Filter .by_property (LectureUnitSchema .COURSE_ID .value ).equal (
123123 course_id
124124 )
125+ unit_filter &= Filter .by_property (LectureUnitSchema .BASE_URL .value ).equal (
126+ base_url
127+ )
125128 unit_results = db .lecture_units .query .fetch_objects (
126129 filters = unit_filter ,
130+ limit = 10_000 ,
127131 return_properties = [
128132 LectureUnitSchema .LECTURE_UNIT_ID .value ,
129133 LectureUnitSchema .LECTURE_NAME .value ,
130134 LectureUnitSchema .LECTURE_UNIT_NAME .value ,
135+ LectureUnitSchema .RELEASE_DATE .value ,
136+ LectureUnitSchema .BASE_URL .value ,
131137 ],
132138 )
133- unit_name_map : dict [int , dict ] = {}
139+ unit_name_map : dict [tuple [ str , int ] , dict ] = {}
134140 for obj in unit_results .objects :
135141 props = obj .properties
136142 lu_id = props .get (LectureUnitSchema .LECTURE_UNIT_ID .value )
137- if lu_id is not None :
138- unit_name_map [lu_id ] = {
143+ unit_base_url = props .get (LectureUnitSchema .BASE_URL .value )
144+ if lu_id is not None and unit_base_url == base_url :
145+ unit_name_map [(unit_base_url , lu_id )] = {
139146 "lecture_name" : props .get (LectureUnitSchema .LECTURE_NAME .value , "" ),
140147 "unit_name" : props .get (
141148 LectureUnitSchema .LECTURE_UNIT_NAME .value , ""
142149 ),
150+ "released" : is_unit_released (props ),
143151 }
144152
145- content = ""
153+ content_parts : list [str ] = []
154+ content_length = 0
155+ visible_chunk_count = 0
146156 units_data : dict [int , dict ] = {}
147- for obj in chunks .objects :
148- props = obj .properties
149- lu_id = props .get (LectureUnitPageChunkSchema .LECTURE_UNIT_ID .value )
150- page = props .get (LectureUnitPageChunkSchema .PAGE_NUMBER .value , 1 )
151- text = props .get (LectureUnitPageChunkSchema .PAGE_TEXT_CONTENT .value , "" )
152- names = unit_name_map .get (lu_id , {})
153- lecture_name = names .get ("lecture_name" , "" )
154- unit_name = names .get ("unit_name" , "" )
155-
156- if text :
157- content += (
157+ offset = 0
158+ while (
159+ offset < _MAX_MCQ_CANDIDATES
160+ and visible_chunk_count < _MAX_MCQ_VISIBLE_CHUNKS
161+ and content_length < _MAX_MCQ_CONTENT_CHARS
162+ ):
163+ page_limit = min (_MCQ_CHUNK_PAGE_SIZE , _MAX_MCQ_CANDIDATES - offset )
164+ chunks = db .lectures .query .fetch_objects (
165+ filters = chunk_filter ,
166+ limit = page_limit ,
167+ offset = offset ,
168+ return_properties = [
169+ LectureUnitPageChunkSchema .LECTURE_UNIT_ID .value ,
170+ LectureUnitPageChunkSchema .LECTURE_ID .value ,
171+ LectureUnitPageChunkSchema .PAGE_NUMBER .value ,
172+ LectureUnitPageChunkSchema .PAGE_TEXT_CONTENT .value ,
173+ LectureUnitPageChunkSchema .HIDDEN_UNTIL .value ,
174+ LectureUnitPageChunkSchema .BASE_URL .value ,
175+ ],
176+ )
177+ if not chunks .objects :
178+ break
179+
180+ for obj in chunks .objects :
181+ props = obj .properties
182+ lu_id = props .get (LectureUnitPageChunkSchema .LECTURE_UNIT_ID .value )
183+ chunk_base_url = props .get (LectureUnitPageChunkSchema .BASE_URL .value )
184+ if chunk_base_url != base_url :
185+ continue
186+ page = props .get (LectureUnitPageChunkSchema .PAGE_NUMBER .value , 1 )
187+ text = props .get (LectureUnitPageChunkSchema .PAGE_TEXT_CONTENT .value , "" )
188+ names = unit_name_map .get ((chunk_base_url , lu_id ), {})
189+ if (
190+ not text
191+ or not names .get ("released" , False )
192+ or not is_slide_visible (props )
193+ ):
194+ continue
195+ lecture_name = names .get ("lecture_name" , "" )
196+ unit_name = names .get ("unit_name" , "" )
197+ fragment = (
158198 f"Lecture: { lecture_name } , Unit: { unit_name } , "
159199 f"Page { page } \n { text } \n \n "
160200 )
161-
162- if lu_id is not None :
163- if lu_id not in units_data :
164- units_data [lu_id ] = {
165- "lecture_unit_id" : lu_id ,
166- "lecture_name" : lecture_name ,
167- "unit_name" : unit_name ,
168- "pages" : set (),
169- }
170- units_data [lu_id ]["pages" ].add (page )
201+ remaining = _MAX_MCQ_CONTENT_CHARS - content_length
202+ fragment = fragment [:remaining ]
203+ content_parts .append (fragment )
204+ content_length += len (fragment )
205+ visible_chunk_count += 1
206+
207+ if lu_id is not None :
208+ if lu_id not in units_data :
209+ units_data [lu_id ] = {
210+ "lecture_unit_id" : lu_id ,
211+ "lecture_name" : lecture_name ,
212+ "unit_name" : unit_name ,
213+ "pages" : set (),
214+ }
215+ units_data [lu_id ]["pages" ].add (page )
216+ if (
217+ visible_chunk_count >= _MAX_MCQ_VISIBLE_CHUNKS
218+ or content_length >= _MAX_MCQ_CONTENT_CHARS
219+ ):
220+ break
221+
222+ offset += len (chunks .objects )
223+ if len (chunks .objects ) < page_limit :
224+ break
171225
172226 lecture_units_meta = []
173227 for data in units_data .values ():
@@ -176,6 +230,7 @@ def retrieve_lecture_content_for_mcq(
176230 del data ["pages" ]
177231 lecture_units_meta .append (data )
178232
233+ content = "" .join (content_parts )
179234 return (content if content .strip () else None ), lecture_units_meta
180235 except Exception as e :
181236 logger .warning ("Failed to fetch lecture summaries for MCQ: %s" , str (e ))
@@ -244,10 +299,12 @@ def mcq_pre_agent_hook(
244299
245300 user_message = get_text_of_latest_user_message (state )
246301 count = getattr (state , "mcq_count" , 1 )
302+ execution_settings = getattr (state .dto , "settings" , None )
247303
248304 lecture_content , _ = retrieve_lecture_content_for_mcq (
249305 db ,
250306 course_id ,
307+ execution_settings .artemis_base_url if execution_settings else "" ,
251308 lecture_id = lecture_id ,
252309 allow_lecture_tool = getattr (state , "allow_lecture_tool" , None ),
253310 )
0 commit comments