Skip to content

Commit d53c560

Browse files
authored
Merge pull request #103 from vectorlessflow/dev
refactor(python): improve error handling and type safety in streaming…
2 parents 7a0ae7c + b88097e commit d53c560

3 files changed

Lines changed: 72 additions & 52 deletions

File tree

python/src/streaming.rs

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,91 +11,91 @@ use pyo3::prelude::*;
1111
use pyo3::types::PyDict;
1212
use pyo3_async_runtimes::tokio::future_into_py;
1313
use 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+
}

rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub use events::{EventEmitter, IndexEvent, QueryEvent, WorkspaceEvent};
8686
pub use metrics::{IndexMetrics, LlmMetricsReport, MetricsReport, RetrievalMetricsReport};
8787

8888
// Retrieval (streaming)
89-
pub use retrieval::RetrieveEvent;
89+
pub use retrieval::{RetrieveEvent, SufficiencyLevel};
9090

9191
// Errors
9292
pub use error::{Error, Result};

rust/src/retrieval/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ pub mod stream;
2525
mod types;
2626

2727
pub use stream::{RetrieveEvent, RetrieveEventReceiver};
28-
pub use types::*;
28+
pub use types::{ReasoningChain, RetrieveResponse, SufficiencyLevel};

0 commit comments

Comments
 (0)