@@ -11,91 +11,91 @@ use pyo3::prelude::*;
1111use pyo3:: types:: PyDict ;
1212use pyo3_async_runtimes:: tokio:: future_into_py;
1313use std:: sync:: Arc ;
14- use tokio:: sync:: { mpsc , Mutex } ;
14+ use tokio:: sync:: { Mutex , mpsc } ;
1515
16- use :: vectorless:: retrieval :: { RetrieveEvent , SufficiencyLevel } ;
16+ use :: vectorless:: { RetrieveEvent , SufficiencyLevel } ;
1717
1818/// Convert a `RetrieveEvent` into a Python dict with a `"type"` key.
19- fn event_to_dict ( event : RetrieveEvent , py : Python < ' _ > ) -> PyObject {
19+ fn event_to_dict ( event : RetrieveEvent , py : Python < ' _ > ) -> PyResult < Bound < ' _ , PyDict > > {
2020 let dict = PyDict :: new ( py) ;
2121 match event {
2222 RetrieveEvent :: Started { query, strategy } => {
23- dict. set_item ( "type" , "started" ) . unwrap ( ) ;
24- dict. set_item ( "query" , query) . unwrap ( ) ;
25- dict. set_item ( "strategy" , strategy) . unwrap ( ) ;
23+ dict. set_item ( "type" , "started" ) ? ;
24+ dict. set_item ( "query" , query) ? ;
25+ dict. set_item ( "strategy" , strategy) ? ;
2626 }
2727 RetrieveEvent :: StageCompleted { stage, elapsed_ms } => {
28- dict. set_item ( "type" , "stage_completed" ) . unwrap ( ) ;
29- dict. set_item ( "stage" , stage) . unwrap ( ) ;
30- dict. set_item ( "elapsed_ms" , elapsed_ms) . unwrap ( ) ;
28+ dict. set_item ( "type" , "stage_completed" ) ? ;
29+ dict. set_item ( "stage" , stage) ? ;
30+ dict. set_item ( "elapsed_ms" , elapsed_ms) ? ;
3131 }
3232 RetrieveEvent :: NodeVisited {
3333 node_id,
3434 title,
3535 score,
3636 } => {
37- dict. set_item ( "type" , "node_visited" ) . unwrap ( ) ;
38- dict. set_item ( "node_id" , node_id) . unwrap ( ) ;
39- dict. set_item ( "title" , title) . unwrap ( ) ;
40- dict. set_item ( "score" , score) . unwrap ( ) ;
37+ dict. set_item ( "type" , "node_visited" ) ? ;
38+ dict. set_item ( "node_id" , node_id) ? ;
39+ dict. set_item ( "title" , title) ? ;
40+ dict. set_item ( "score" , score) ? ;
4141 }
4242 RetrieveEvent :: ContentFound {
4343 node_id,
4444 title,
4545 preview,
4646 score,
4747 } => {
48- dict. set_item ( "type" , "content_found" ) . unwrap ( ) ;
49- dict. set_item ( "node_id" , node_id) . unwrap ( ) ;
50- dict. set_item ( "title" , title) . unwrap ( ) ;
51- dict. set_item ( "preview" , preview) . unwrap ( ) ;
52- dict. set_item ( "score" , score) . unwrap ( ) ;
48+ dict. set_item ( "type" , "content_found" ) ? ;
49+ dict. set_item ( "node_id" , node_id) ? ;
50+ dict. set_item ( "title" , title) ? ;
51+ dict. set_item ( "preview" , preview) ? ;
52+ dict. set_item ( "score" , score) ? ;
5353 }
5454 RetrieveEvent :: Backtracking { from, to, reason } => {
55- dict. set_item ( "type" , "backtracking" ) . unwrap ( ) ;
56- dict. set_item ( "from" , from) . unwrap ( ) ;
57- dict. set_item ( "to" , to) . unwrap ( ) ;
58- dict. set_item ( "reason" , reason) . unwrap ( ) ;
55+ dict. set_item ( "type" , "backtracking" ) ? ;
56+ dict. set_item ( "from" , from) ? ;
57+ dict. set_item ( "to" , to) ? ;
58+ dict. set_item ( "reason" , reason) ? ;
5959 }
6060 RetrieveEvent :: SufficiencyCheck { level, tokens } => {
6161 let level_str = match level {
6262 SufficiencyLevel :: Sufficient => "sufficient" ,
6363 SufficiencyLevel :: PartialSufficient => "partial_sufficient" ,
6464 SufficiencyLevel :: Insufficient => "insufficient" ,
6565 } ;
66- dict. set_item ( "type" , "sufficiency_check" ) . unwrap ( ) ;
67- dict. set_item ( "level" , level_str) . unwrap ( ) ;
68- dict. set_item ( "tokens" , tokens) . unwrap ( ) ;
66+ dict. set_item ( "type" , "sufficiency_check" ) ? ;
67+ dict. set_item ( "level" , level_str) ? ;
68+ dict. set_item ( "tokens" , tokens) ? ;
6969 }
7070 RetrieveEvent :: Completed { response } => {
71- dict. set_item ( "type" , "completed" ) . unwrap ( ) ;
72- dict. set_item ( "confidence" , response. confidence ) . unwrap ( ) ;
73- dict. set_item ( "is_sufficient" , response. is_sufficient ) . unwrap ( ) ;
74- dict. set_item ( "strategy_used" , response. strategy_used ) . unwrap ( ) ;
75- dict. set_item ( "tokens_used" , response. tokens_used ) . unwrap ( ) ;
76- dict. set_item ( "content" , response. content ) . unwrap ( ) ;
71+ dict. set_item ( "type" , "completed" ) ? ;
72+ dict. set_item ( "confidence" , response. confidence ) ? ;
73+ dict. set_item ( "is_sufficient" , response. is_sufficient ) ? ;
74+ dict. set_item ( "strategy_used" , response. strategy_used ) ? ;
75+ dict. set_item ( "tokens_used" , response. tokens_used ) ? ;
76+ dict. set_item ( "content" , response. content ) ? ;
7777
78- let results: Vec < PyObject > = response
78+ let results: Vec < Bound < ' _ , PyDict > > = response
7979 . results
8080 . into_iter ( )
8181 . map ( |r| {
8282 let rd = PyDict :: new ( py) ;
83- rd. set_item ( "node_id" , & r. node_id ) . unwrap ( ) ;
84- rd. set_item ( "title" , & r. title ) . unwrap ( ) ;
85- rd. set_item ( "content" , & r. content ) . unwrap ( ) ;
86- rd. set_item ( "score" , r. score ) . unwrap ( ) ;
87- rd. set_item ( "depth" , r. depth ) . unwrap ( ) ;
88- rd . into ( )
83+ rd. set_item ( "node_id" , & r. node_id ) ? ;
84+ rd. set_item ( "title" , & r. title ) ? ;
85+ rd. set_item ( "content" , & r. content ) ? ;
86+ rd. set_item ( "score" , r. score ) ? ;
87+ rd. set_item ( "depth" , r. depth ) ? ;
88+ Ok ( rd )
8989 } )
90- . collect ( ) ;
91- dict. set_item ( "results" , results) . unwrap ( ) ;
90+ . collect :: < PyResult < Vec < _ > > > ( ) ? ;
91+ dict. set_item ( "results" , results) ? ;
9292 }
9393 RetrieveEvent :: Error { message } => {
94- dict. set_item ( "type" , "error" ) . unwrap ( ) ;
95- dict. set_item ( "message" , message) . unwrap ( ) ;
94+ dict. set_item ( "type" , "error" ) ? ;
95+ dict. set_item ( "message" , message) ? ;
9696 }
9797 }
98- dict . into ( )
98+ Ok ( dict )
9999}
100100
101101/// Python-facing async iterator over streaming retrieval events.
@@ -125,12 +125,13 @@ impl PyStreamingQuery {
125125 }
126126
127127 fn __anext__ < ' py > ( & self , py : Python < ' py > ) -> PyResult < Bound < ' py , PyAny > > {
128- let rx = Arc :: clone ( & self . rx ) ;
128+ let rx: Arc < Mutex < Option < mpsc :: Receiver < RetrieveEvent > > > > = Arc :: clone ( & self . rx ) ;
129129 future_into_py ( py, async move {
130130 let mut guard = rx. lock ( ) . await ;
131- match guard. as_mut ( ) {
131+ let receiver: & mut Option < mpsc:: Receiver < RetrieveEvent > > = & mut * guard;
132+ match receiver {
132133 None => Err ( PyStopAsyncIteration :: new_err ( "stream exhausted" ) ) ,
133- Some ( receiver ) => match receiver . recv ( ) . await {
134+ Some ( rx ) => match rx . recv ( ) . await {
134135 Some ( event) => {
135136 let is_terminal = matches ! (
136137 & event,
@@ -139,10 +140,15 @@ impl PyStreamingQuery {
139140 if is_terminal {
140141 * guard = None ;
141142 }
142- // Convert to Python dict — safe because future_into_py
143- // ensures we're on a thread that can acquire the GIL.
144- let obj = Python :: with_gil ( |py| event_to_dict ( event, py) ) ;
145- Ok ( obj)
143+ // We cannot convert to dict here (no Python token in async context).
144+ // Instead, store the event and convert on the Python side.
145+ // PyO3 0.28: future_into_py resolves on the Python thread,
146+ // so we use Python::with_gil equivalent via pyo3_async_runtimes.
147+ //
148+ // The cleanest approach: wrap in a PyO3-compatible type.
149+ // Since RetrieveEvent doesn't implement IntoPyObject, we convert
150+ // to a simple serializable form.
151+ Ok ( SerializedEvent ( event) )
146152 }
147153 None => {
148154 * guard = None ;
@@ -157,3 +163,17 @@ impl PyStreamingQuery {
157163 "StreamingQuery(...)" . to_string ( )
158164 }
159165}
166+
167+ /// Wrapper to carry a RetrieveEvent across the async boundary
168+ /// and convert it to a dict on the Python thread.
169+ struct SerializedEvent ( RetrieveEvent ) ;
170+
171+ impl < ' py > IntoPyObject < ' py > for SerializedEvent {
172+ type Target = PyDict ;
173+ type Output = Bound < ' py , Self :: Target > ;
174+ type Error = PyErr ;
175+
176+ fn into_pyobject ( self , py : Python < ' py > ) -> Result < Self :: Output , Self :: Error > {
177+ event_to_dict ( self . 0 , py)
178+ }
179+ }
0 commit comments